1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.annotation.AttrRes;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.StyleRes;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.database.DataSetObserver;
26import android.graphics.Rect;
27import android.graphics.drawable.Drawable;
28import android.os.Build;
29import android.os.Handler;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.Gravity;
33import android.view.KeyEvent;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.View.MeasureSpec;
37import android.view.View.OnTouchListener;
38import android.view.ViewGroup;
39import android.view.ViewParent;
40import android.view.WindowManager;
41import android.widget.AdapterView.OnItemSelectedListener;
42
43import com.android.internal.R;
44import com.android.internal.view.menu.ShowableListMenu;
45
46/**
47 * A ListPopupWindow anchors itself to a host view and displays a
48 * list of choices.
49 *
50 * <p>ListPopupWindow contains a number of tricky behaviors surrounding
51 * positioning, scrolling parents to fit the dropdown, interacting
52 * sanely with the IME if present, and others.
53 *
54 * @see android.widget.AutoCompleteTextView
55 * @see android.widget.Spinner
56 */
57public class ListPopupWindow implements ShowableListMenu {
58    private static final String TAG = "ListPopupWindow";
59    private static final boolean DEBUG = false;
60
61    /**
62     * This value controls the length of time that the user
63     * must leave a pointer down without scrolling to expand
64     * the autocomplete dropdown list to cover the IME.
65     */
66    private static final int EXPAND_LIST_TIMEOUT = 250;
67
68    private Context mContext;
69    private ListAdapter mAdapter;
70    private DropDownListView mDropDownList;
71
72    private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
73    private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
74    private int mDropDownHorizontalOffset;
75    private int mDropDownVerticalOffset;
76    private int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
77    private boolean mDropDownVerticalOffsetSet;
78    private boolean mIsAnimatedFromAnchor = true;
79    private boolean mOverlapAnchor;
80    private boolean mOverlapAnchorSet;
81
82    private int mDropDownGravity = Gravity.NO_GRAVITY;
83
84    private boolean mDropDownAlwaysVisible = false;
85    private boolean mForceIgnoreOutsideTouch = false;
86    int mListItemExpandMaximum = Integer.MAX_VALUE;
87
88    private View mPromptView;
89    private int mPromptPosition = POSITION_PROMPT_ABOVE;
90
91    private DataSetObserver mObserver;
92
93    private View mDropDownAnchorView;
94
95    private Drawable mDropDownListHighlight;
96
97    private AdapterView.OnItemClickListener mItemClickListener;
98    private AdapterView.OnItemSelectedListener mItemSelectedListener;
99
100    private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
101    private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
102    private final PopupScrollListener mScrollListener = new PopupScrollListener();
103    private final ListSelectorHider mHideSelector = new ListSelectorHider();
104    private Runnable mShowDropDownRunnable;
105
106    private final Handler mHandler;
107
108    private final Rect mTempRect = new Rect();
109
110    /**
111     * Optional anchor-relative bounds to be used as the transition epicenter.
112     * When {@code null}, the anchor bounds are used as the epicenter.
113     */
114    private Rect mEpicenterBounds;
115
116    private boolean mModal;
117
118    PopupWindow mPopup;
119
120    /**
121     * The provided prompt view should appear above list content.
122     *
123     * @see #setPromptPosition(int)
124     * @see #getPromptPosition()
125     * @see #setPromptView(View)
126     */
127    public static final int POSITION_PROMPT_ABOVE = 0;
128
129    /**
130     * The provided prompt view should appear below list content.
131     *
132     * @see #setPromptPosition(int)
133     * @see #getPromptPosition()
134     * @see #setPromptView(View)
135     */
136    public static final int POSITION_PROMPT_BELOW = 1;
137
138    /**
139     * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
140     * If used to specify a popup width, the popup will match the width of the anchor view.
141     * If used to specify a popup height, the popup will fill available space.
142     */
143    public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
144
145    /**
146     * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
147     * If used to specify a popup width, the popup will use the width of its content.
148     */
149    public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
150
151    /**
152     * Mode for {@link #setInputMethodMode(int)}: the requirements for the
153     * input method should be based on the focusability of the popup.  That is
154     * if it is focusable than it needs to work with the input method, else
155     * it doesn't.
156     */
157    public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
158
159    /**
160     * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
161     * work with an input method, regardless of whether it is focusable.  This
162     * means that it will always be displayed so that the user can also operate
163     * the input method while it is shown.
164     */
165    public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED;
166
167    /**
168     * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
169     * work with an input method, regardless of whether it is focusable.  This
170     * means that it will always be displayed to use as much space on the
171     * screen as needed, regardless of whether this covers the input method.
172     */
173    public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
174
175    /**
176     * Create a new, empty popup window capable of displaying items from a ListAdapter.
177     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
178     *
179     * @param context Context used for contained views.
180     */
181    public ListPopupWindow(@NonNull Context context) {
182        this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0);
183    }
184
185    /**
186     * Create a new, empty popup window capable of displaying items from a ListAdapter.
187     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
188     *
189     * @param context Context used for contained views.
190     * @param attrs Attributes from inflating parent views used to style the popup.
191     */
192    public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs) {
193        this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0);
194    }
195
196    /**
197     * Create a new, empty popup window capable of displaying items from a ListAdapter.
198     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
199     *
200     * @param context Context used for contained views.
201     * @param attrs Attributes from inflating parent views used to style the popup.
202     * @param defStyleAttr Default style attribute to use for popup content.
203     */
204    public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs,
205            @AttrRes int defStyleAttr) {
206        this(context, attrs, defStyleAttr, 0);
207    }
208
209    /**
210     * Create a new, empty popup window capable of displaying items from a ListAdapter.
211     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
212     *
213     * @param context Context used for contained views.
214     * @param attrs Attributes from inflating parent views used to style the popup.
215     * @param defStyleAttr Style attribute to read for default styling of popup content.
216     * @param defStyleRes Style resource ID to use for default styling of popup content.
217     */
218    public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs,
219            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
220        mContext = context;
221        mHandler = new Handler(context.getMainLooper());
222
223        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow,
224                defStyleAttr, defStyleRes);
225        mDropDownHorizontalOffset = a.getDimensionPixelOffset(
226                R.styleable.ListPopupWindow_dropDownHorizontalOffset, 0);
227        mDropDownVerticalOffset = a.getDimensionPixelOffset(
228                R.styleable.ListPopupWindow_dropDownVerticalOffset, 0);
229        if (mDropDownVerticalOffset != 0) {
230            mDropDownVerticalOffsetSet = true;
231        }
232        a.recycle();
233
234        mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
235        mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
236    }
237
238    /**
239     * Sets the adapter that provides the data and the views to represent the data
240     * in this popup window.
241     *
242     * @param adapter The adapter to use to create this window's content.
243     */
244    public void setAdapter(@Nullable ListAdapter adapter) {
245        if (mObserver == null) {
246            mObserver = new PopupDataSetObserver();
247        } else if (mAdapter != null) {
248            mAdapter.unregisterDataSetObserver(mObserver);
249        }
250        mAdapter = adapter;
251        if (mAdapter != null) {
252            adapter.registerDataSetObserver(mObserver);
253        }
254
255        if (mDropDownList != null) {
256            mDropDownList.setAdapter(mAdapter);
257        }
258    }
259
260    /**
261     * Set where the optional prompt view should appear. The default is
262     * {@link #POSITION_PROMPT_ABOVE}.
263     *
264     * @param position A position constant declaring where the prompt should be displayed.
265     *
266     * @see #POSITION_PROMPT_ABOVE
267     * @see #POSITION_PROMPT_BELOW
268     */
269    public void setPromptPosition(int position) {
270        mPromptPosition = position;
271    }
272
273    /**
274     * @return Where the optional prompt view should appear.
275     *
276     * @see #POSITION_PROMPT_ABOVE
277     * @see #POSITION_PROMPT_BELOW
278     */
279    public int getPromptPosition() {
280        return mPromptPosition;
281    }
282
283    /**
284     * Set whether this window should be modal when shown.
285     *
286     * <p>If a popup window is modal, it will receive all touch and key input.
287     * If the user touches outside the popup window's content area the popup window
288     * will be dismissed.
289     *
290     * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
291     */
292    public void setModal(boolean modal) {
293        mModal = modal;
294        mPopup.setFocusable(modal);
295    }
296
297    /**
298     * Returns whether the popup window will be modal when shown.
299     *
300     * @return {@code true} if the popup window will be modal, {@code false} otherwise.
301     */
302    public boolean isModal() {
303        return mModal;
304    }
305
306    /**
307     * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
308     * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
309     * ignore outside touch even when the drop down is not set to always visible.
310     *
311     * @hide Used only by AutoCompleteTextView to handle some internal special cases.
312     */
313    public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
314        mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
315    }
316
317    /**
318     * Sets whether the drop-down should remain visible under certain conditions.
319     *
320     * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
321     * of the size or content of the list.  {@link #getBackground()} will fill any space
322     * that is not used by the list.
323     *
324     * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
325     *
326     * @hide Only used by AutoCompleteTextView under special conditions.
327     */
328    public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
329        mDropDownAlwaysVisible = dropDownAlwaysVisible;
330    }
331
332    /**
333     * @return Whether the drop-down is visible under special conditions.
334     *
335     * @hide Only used by AutoCompleteTextView under special conditions.
336     */
337    public boolean isDropDownAlwaysVisible() {
338        return mDropDownAlwaysVisible;
339    }
340
341    /**
342     * Sets the operating mode for the soft input area.
343     *
344     * @param mode The desired mode, see
345     *        {@link android.view.WindowManager.LayoutParams#softInputMode}
346     *        for the full list
347     *
348     * @see android.view.WindowManager.LayoutParams#softInputMode
349     * @see #getSoftInputMode()
350     */
351    public void setSoftInputMode(int mode) {
352        mPopup.setSoftInputMode(mode);
353    }
354
355    /**
356     * Returns the current value in {@link #setSoftInputMode(int)}.
357     *
358     * @see #setSoftInputMode(int)
359     * @see android.view.WindowManager.LayoutParams#softInputMode
360     */
361    public int getSoftInputMode() {
362        return mPopup.getSoftInputMode();
363    }
364
365    /**
366     * Sets a drawable to use as the list item selector.
367     *
368     * @param selector List selector drawable to use in the popup.
369     */
370    public void setListSelector(Drawable selector) {
371        mDropDownListHighlight = selector;
372    }
373
374    /**
375     * @return The background drawable for the popup window.
376     */
377    public @Nullable Drawable getBackground() {
378        return mPopup.getBackground();
379    }
380
381    /**
382     * Sets a drawable to be the background for the popup window.
383     *
384     * @param d A drawable to set as the background.
385     */
386    public void setBackgroundDrawable(@Nullable Drawable d) {
387        mPopup.setBackgroundDrawable(d);
388    }
389
390    /**
391     * Set an animation style to use when the popup window is shown or dismissed.
392     *
393     * @param animationStyle Animation style to use.
394     */
395    public void setAnimationStyle(@StyleRes int animationStyle) {
396        mPopup.setAnimationStyle(animationStyle);
397    }
398
399    /**
400     * Returns the animation style that will be used when the popup window is
401     * shown or dismissed.
402     *
403     * @return Animation style that will be used.
404     */
405    public @StyleRes int getAnimationStyle() {
406        return mPopup.getAnimationStyle();
407    }
408
409    /**
410     * Returns the view that will be used to anchor this popup.
411     *
412     * @return The popup's anchor view
413     */
414    public @Nullable View getAnchorView() {
415        return mDropDownAnchorView;
416    }
417
418    /**
419     * Sets the popup's anchor view. This popup will always be positioned relative to
420     * the anchor view when shown.
421     *
422     * @param anchor The view to use as an anchor.
423     */
424    public void setAnchorView(@Nullable View anchor) {
425        mDropDownAnchorView = anchor;
426    }
427
428    /**
429     * @return The horizontal offset of the popup from its anchor in pixels.
430     */
431    public int getHorizontalOffset() {
432        return mDropDownHorizontalOffset;
433    }
434
435    /**
436     * Set the horizontal offset of this popup from its anchor view in pixels.
437     *
438     * @param offset The horizontal offset of the popup from its anchor.
439     */
440    public void setHorizontalOffset(int offset) {
441        mDropDownHorizontalOffset = offset;
442    }
443
444    /**
445     * @return The vertical offset of the popup from its anchor in pixels.
446     */
447    public int getVerticalOffset() {
448        if (!mDropDownVerticalOffsetSet) {
449            return 0;
450        }
451        return mDropDownVerticalOffset;
452    }
453
454    /**
455     * Set the vertical offset of this popup from its anchor view in pixels.
456     *
457     * @param offset The vertical offset of the popup from its anchor.
458     */
459    public void setVerticalOffset(int offset) {
460        mDropDownVerticalOffset = offset;
461        mDropDownVerticalOffsetSet = true;
462    }
463
464    /**
465     * Specifies the anchor-relative bounds of the popup's transition
466     * epicenter.
467     *
468     * @param bounds anchor-relative bounds
469     * @hide
470     */
471    public void setEpicenterBounds(Rect bounds) {
472        mEpicenterBounds = bounds;
473    }
474
475    /**
476     * Set the gravity of the dropdown list. This is commonly used to
477     * set gravity to START or END for alignment with the anchor.
478     *
479     * @param gravity Gravity value to use
480     */
481    public void setDropDownGravity(int gravity) {
482        mDropDownGravity = gravity;
483    }
484
485    /**
486     * @return The width of the popup window in pixels.
487     */
488    public int getWidth() {
489        return mDropDownWidth;
490    }
491
492    /**
493     * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
494     * or {@link #WRAP_CONTENT}.
495     *
496     * @param width Width of the popup window.
497     */
498    public void setWidth(int width) {
499        mDropDownWidth = width;
500    }
501
502    /**
503     * Sets the width of the popup window by the size of its content. The final width may be
504     * larger to accommodate styled window dressing.
505     *
506     * @param width Desired width of content in pixels.
507     */
508    public void setContentWidth(int width) {
509        Drawable popupBackground = mPopup.getBackground();
510        if (popupBackground != null) {
511            popupBackground.getPadding(mTempRect);
512            mDropDownWidth = mTempRect.left + mTempRect.right + width;
513        } else {
514            setWidth(width);
515        }
516    }
517
518    /**
519     * @return The height of the popup window in pixels.
520     */
521    public int getHeight() {
522        return mDropDownHeight;
523    }
524
525    /**
526     * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
527     *
528     * @param height Height of the popup window must be a positive value,
529     *               {@link #MATCH_PARENT}, or {@link #WRAP_CONTENT}.
530     *
531     * @throws IllegalArgumentException if height is set to negative value
532     */
533    public void setHeight(int height) {
534        if (height < 0 && ViewGroup.LayoutParams.WRAP_CONTENT != height
535                && ViewGroup.LayoutParams.MATCH_PARENT != height) {
536            if (mContext.getApplicationInfo().targetSdkVersion
537                    < Build.VERSION_CODES.O) {
538                Log.e(TAG, "Negative value " + height + " passed to ListPopupWindow#setHeight"
539                        + " produces undefined results");
540            } else {
541                throw new IllegalArgumentException(
542                        "Invalid height. Must be a positive value, MATCH_PARENT, or WRAP_CONTENT.");
543            }
544        }
545        mDropDownHeight = height;
546    }
547
548    /**
549     * Set the layout type for this popup window.
550     * <p>
551     * See {@link WindowManager.LayoutParams#type} for possible values.
552     *
553     * @param layoutType Layout type for this window.
554     *
555     * @see WindowManager.LayoutParams#type
556     */
557    public void setWindowLayoutType(int layoutType) {
558        mDropDownWindowLayoutType = layoutType;
559    }
560
561    /**
562     * Sets a listener to receive events when a list item is clicked.
563     *
564     * @param clickListener Listener to register
565     *
566     * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)
567     */
568    public void setOnItemClickListener(@Nullable AdapterView.OnItemClickListener clickListener) {
569        mItemClickListener = clickListener;
570    }
571
572    /**
573     * Sets a listener to receive events when a list item is selected.
574     *
575     * @param selectedListener Listener to register.
576     *
577     * @see ListView#setOnItemSelectedListener(OnItemSelectedListener)
578     */
579    public void setOnItemSelectedListener(@Nullable OnItemSelectedListener selectedListener) {
580        mItemSelectedListener = selectedListener;
581    }
582
583    /**
584     * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
585     * is controlled by {@link #setPromptPosition(int)}.
586     *
587     * @param prompt View to use as an informational prompt.
588     */
589    public void setPromptView(@Nullable View prompt) {
590        boolean showing = isShowing();
591        if (showing) {
592            removePromptView();
593        }
594        mPromptView = prompt;
595        if (showing) {
596            show();
597        }
598    }
599
600    /**
601     * Post a {@link #show()} call to the UI thread.
602     */
603    public void postShow() {
604        mHandler.post(mShowDropDownRunnable);
605    }
606
607    /**
608     * Show the popup list. If the list is already showing, this method
609     * will recalculate the popup's size and position.
610     */
611    @Override
612    public void show() {
613        int height = buildDropDown();
614
615        final boolean noInputMethod = isInputMethodNotNeeded();
616        mPopup.setAllowScrollingAnchorParent(!noInputMethod);
617        mPopup.setWindowLayoutType(mDropDownWindowLayoutType);
618
619        if (mPopup.isShowing()) {
620            if (!getAnchorView().isAttachedToWindow()) {
621                //Don't update position if the anchor view is detached from window.
622                return;
623            }
624            final int widthSpec;
625            if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
626                // The call to PopupWindow's update method below can accept -1 for any
627                // value you do not want to update.
628                widthSpec = -1;
629            } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
630                widthSpec = getAnchorView().getWidth();
631            } else {
632                widthSpec = mDropDownWidth;
633            }
634
635            final int heightSpec;
636            if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
637                // The call to PopupWindow's update method below can accept -1 for any
638                // value you do not want to update.
639                heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
640                if (noInputMethod) {
641                    mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
642                            ViewGroup.LayoutParams.MATCH_PARENT : 0);
643                    mPopup.setHeight(0);
644                } else {
645                    mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
646                                    ViewGroup.LayoutParams.MATCH_PARENT : 0);
647                    mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
648                }
649            } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
650                heightSpec = height;
651            } else {
652                heightSpec = mDropDownHeight;
653            }
654
655            mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
656
657            mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
658                            mDropDownVerticalOffset, (widthSpec < 0)? -1 : widthSpec,
659                            (heightSpec < 0)? -1 : heightSpec);
660        } else {
661            final int widthSpec;
662            if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
663                widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
664            } else {
665                if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
666                    widthSpec = getAnchorView().getWidth();
667                } else {
668                    widthSpec = mDropDownWidth;
669                }
670            }
671
672            final int heightSpec;
673            if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
674                heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
675            } else {
676                if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
677                    heightSpec = height;
678                } else {
679                    heightSpec = mDropDownHeight;
680                }
681            }
682
683            mPopup.setWidth(widthSpec);
684            mPopup.setHeight(heightSpec);
685            mPopup.setClipToScreenEnabled(true);
686
687            // use outside touchable to dismiss drop down when touching outside of it, so
688            // only set this if the dropdown is not always visible
689            mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
690            mPopup.setTouchInterceptor(mTouchInterceptor);
691            mPopup.setEpicenterBounds(mEpicenterBounds);
692            if (mOverlapAnchorSet) {
693                mPopup.setOverlapAnchor(mOverlapAnchor);
694            }
695            mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset,
696                    mDropDownVerticalOffset, mDropDownGravity);
697            mDropDownList.setSelection(ListView.INVALID_POSITION);
698
699            if (!mModal || mDropDownList.isInTouchMode()) {
700                clearListSelection();
701            }
702            if (!mModal) {
703                mHandler.post(mHideSelector);
704            }
705        }
706    }
707
708    /**
709     * Dismiss the popup window.
710     */
711    @Override
712    public void dismiss() {
713        mPopup.dismiss();
714        removePromptView();
715        mPopup.setContentView(null);
716        mDropDownList = null;
717        mHandler.removeCallbacks(mResizePopupRunnable);
718    }
719
720    /**
721     * Set a listener to receive a callback when the popup is dismissed.
722     *
723     * @param listener Listener that will be notified when the popup is dismissed.
724     */
725    public void setOnDismissListener(@Nullable PopupWindow.OnDismissListener listener) {
726        mPopup.setOnDismissListener(listener);
727    }
728
729    private void removePromptView() {
730        if (mPromptView != null) {
731            final ViewParent parent = mPromptView.getParent();
732            if (parent instanceof ViewGroup) {
733                final ViewGroup group = (ViewGroup) parent;
734                group.removeView(mPromptView);
735            }
736        }
737    }
738
739    /**
740     * Control how the popup operates with an input method: one of
741     * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
742     * or {@link #INPUT_METHOD_NOT_NEEDED}.
743     *
744     * <p>If the popup is showing, calling this method will take effect only
745     * the next time the popup is shown or through a manual call to the {@link #show()}
746     * method.</p>
747     *
748     * @see #getInputMethodMode()
749     * @see #show()
750     */
751    public void setInputMethodMode(int mode) {
752        mPopup.setInputMethodMode(mode);
753    }
754
755    /**
756     * Return the current value in {@link #setInputMethodMode(int)}.
757     *
758     * @see #setInputMethodMode(int)
759     */
760    public int getInputMethodMode() {
761        return mPopup.getInputMethodMode();
762    }
763
764    /**
765     * Set the selected position of the list.
766     * Only valid when {@link #isShowing()} == {@code true}.
767     *
768     * @param position List position to set as selected.
769     */
770    public void setSelection(int position) {
771        DropDownListView list = mDropDownList;
772        if (isShowing() && list != null) {
773            list.setListSelectionHidden(false);
774            list.setSelection(position);
775            if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
776                list.setItemChecked(position, true);
777            }
778        }
779    }
780
781    /**
782     * Clear any current list selection.
783     * Only valid when {@link #isShowing()} == {@code true}.
784     */
785    public void clearListSelection() {
786        final DropDownListView list = mDropDownList;
787        if (list != null) {
788            // WARNING: Please read the comment where mListSelectionHidden is declared
789            list.setListSelectionHidden(true);
790            list.hideSelector();
791            list.requestLayout();
792        }
793    }
794
795    /**
796     * @return {@code true} if the popup is currently showing, {@code false} otherwise.
797     */
798    @Override
799    public boolean isShowing() {
800        return mPopup.isShowing();
801    }
802
803    /**
804     * @return {@code true} if this popup is configured to assume the user does not need
805     * to interact with the IME while it is showing, {@code false} otherwise.
806     */
807    public boolean isInputMethodNotNeeded() {
808        return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
809    }
810
811    /**
812     * Perform an item click operation on the specified list adapter position.
813     *
814     * @param position Adapter position for performing the click
815     * @return true if the click action could be performed, false if not.
816     *         (e.g. if the popup was not showing, this method would return false.)
817     */
818    public boolean performItemClick(int position) {
819        if (isShowing()) {
820            if (mItemClickListener != null) {
821                final DropDownListView list = mDropDownList;
822                final View child = list.getChildAt(position - list.getFirstVisiblePosition());
823                final ListAdapter adapter = list.getAdapter();
824                mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
825            }
826            return true;
827        }
828        return false;
829    }
830
831    /**
832     * @return The currently selected item or null if the popup is not showing.
833     */
834    public @Nullable Object getSelectedItem() {
835        if (!isShowing()) {
836            return null;
837        }
838        return mDropDownList.getSelectedItem();
839    }
840
841    /**
842     * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
843     * if {@link #isShowing()} == {@code false}.
844     *
845     * @see ListView#getSelectedItemPosition()
846     */
847    public int getSelectedItemPosition() {
848        if (!isShowing()) {
849            return ListView.INVALID_POSITION;
850        }
851        return mDropDownList.getSelectedItemPosition();
852    }
853
854    /**
855     * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
856     * if {@link #isShowing()} == {@code false}.
857     *
858     * @see ListView#getSelectedItemId()
859     */
860    public long getSelectedItemId() {
861        if (!isShowing()) {
862            return ListView.INVALID_ROW_ID;
863        }
864        return mDropDownList.getSelectedItemId();
865    }
866
867    /**
868     * @return The View for the currently selected item or null if
869     * {@link #isShowing()} == {@code false}.
870     *
871     * @see ListView#getSelectedView()
872     */
873    public @Nullable View getSelectedView() {
874        if (!isShowing()) {
875            return null;
876        }
877        return mDropDownList.getSelectedView();
878    }
879
880    /**
881     * @return The {@link ListView} displayed within the popup window.
882     * Only valid when {@link #isShowing()} == {@code true}.
883     */
884    @Override
885    public @Nullable ListView getListView() {
886        return mDropDownList;
887    }
888
889    @NonNull DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
890        return new DropDownListView(context, hijackFocus);
891    }
892
893    /**
894     * The maximum number of list items that can be visible and still have
895     * the list expand when touched.
896     *
897     * @param max Max number of items that can be visible and still allow the list to expand.
898     */
899    void setListItemExpandMax(int max) {
900        mListItemExpandMaximum = max;
901    }
902
903    /**
904     * Filter key down events. By forwarding key down events to this function,
905     * views using non-modal ListPopupWindow can have it handle key selection of items.
906     *
907     * @param keyCode keyCode param passed to the host view's onKeyDown
908     * @param event event param passed to the host view's onKeyDown
909     * @return true if the event was handled, false if it was ignored.
910     *
911     * @see #setModal(boolean)
912     * @see #onKeyUp(int, KeyEvent)
913     */
914    public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
915        // when the drop down is shown, we drive it directly
916        if (isShowing()) {
917            // the key events are forwarded to the list in the drop down view
918            // note that ListView handles space but we don't want that to happen
919            // also if selection is not currently in the drop down, then don't
920            // let center or enter presses go there since that would cause it
921            // to select one of its items
922            if (keyCode != KeyEvent.KEYCODE_SPACE
923                    && (mDropDownList.getSelectedItemPosition() >= 0
924                            || !KeyEvent.isConfirmKey(keyCode))) {
925                int curIndex = mDropDownList.getSelectedItemPosition();
926                boolean consumed;
927
928                final boolean below = !mPopup.isAboveAnchor();
929
930                final ListAdapter adapter = mAdapter;
931
932                boolean allEnabled;
933                int firstItem = Integer.MAX_VALUE;
934                int lastItem = Integer.MIN_VALUE;
935
936                if (adapter != null) {
937                    allEnabled = adapter.areAllItemsEnabled();
938                    firstItem = allEnabled ? 0 :
939                            mDropDownList.lookForSelectablePosition(0, true);
940                    lastItem = allEnabled ? adapter.getCount() - 1 :
941                            mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
942                }
943
944                if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
945                        (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
946                    // When the selection is at the top, we block the key
947                    // event to prevent focus from moving.
948                    clearListSelection();
949                    mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
950                    show();
951                    return true;
952                } else {
953                    // WARNING: Please read the comment where mListSelectionHidden
954                    //          is declared
955                    mDropDownList.setListSelectionHidden(false);
956                }
957
958                consumed = mDropDownList.onKeyDown(keyCode, event);
959                if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
960
961                if (consumed) {
962                    // If it handled the key event, then the user is
963                    // navigating in the list, so we should put it in front.
964                    mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
965                    // Here's a little trick we need to do to make sure that
966                    // the list view is actually showing its focus indicator,
967                    // by ensuring it has focus and getting its window out
968                    // of touch mode.
969                    mDropDownList.requestFocusFromTouch();
970                    show();
971
972                    switch (keyCode) {
973                        // avoid passing the focus from the text view to the
974                        // next component
975                        case KeyEvent.KEYCODE_ENTER:
976                        case KeyEvent.KEYCODE_DPAD_CENTER:
977                        case KeyEvent.KEYCODE_DPAD_DOWN:
978                        case KeyEvent.KEYCODE_DPAD_UP:
979                            return true;
980                    }
981                } else {
982                    if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
983                        // when the selection is at the bottom, we block the
984                        // event to avoid going to the next focusable widget
985                        if (curIndex == lastItem) {
986                            return true;
987                        }
988                    } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
989                            curIndex == firstItem) {
990                        return true;
991                    }
992                }
993            }
994        }
995
996        return false;
997    }
998
999    /**
1000     * Filter key up events. By forwarding key up events to this function,
1001     * views using non-modal ListPopupWindow can have it handle key selection of items.
1002     *
1003     * @param keyCode keyCode param passed to the host view's onKeyUp
1004     * @param event event param passed to the host view's onKeyUp
1005     * @return true if the event was handled, false if it was ignored.
1006     *
1007     * @see #setModal(boolean)
1008     * @see #onKeyDown(int, KeyEvent)
1009     */
1010    public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
1011        if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
1012            boolean consumed = mDropDownList.onKeyUp(keyCode, event);
1013            if (consumed && KeyEvent.isConfirmKey(keyCode)) {
1014                // if the list accepts the key events and the key event was a click, the text view
1015                // gets the selected item from the drop down as its content
1016                dismiss();
1017            }
1018            return consumed;
1019        }
1020        return false;
1021    }
1022
1023    /**
1024     * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
1025     * events to this function, views using ListPopupWindow can have it dismiss the popup
1026     * when the back key is pressed.
1027     *
1028     * @param keyCode keyCode param passed to the host view's onKeyPreIme
1029     * @param event event param passed to the host view's onKeyPreIme
1030     * @return true if the event was handled, false if it was ignored.
1031     *
1032     * @see #setModal(boolean)
1033     */
1034    public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {
1035        if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
1036            // special case for the back key, we do not even try to send it
1037            // to the drop down list but instead, consume it immediately
1038            final View anchorView = mDropDownAnchorView;
1039            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
1040                KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
1041                if (state != null) {
1042                    state.startTracking(event, this);
1043                }
1044                return true;
1045            } else if (event.getAction() == KeyEvent.ACTION_UP) {
1046                KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
1047                if (state != null) {
1048                    state.handleUpEvent(event);
1049                }
1050                if (event.isTracking() && !event.isCanceled()) {
1051                    dismiss();
1052                    return true;
1053                }
1054            }
1055        }
1056        return false;
1057    }
1058
1059    /**
1060     * Returns an {@link OnTouchListener} that can be added to the source view
1061     * to implement drag-to-open behavior. Generally, the source view should be
1062     * the same view that was passed to {@link #setAnchorView}.
1063     * <p>
1064     * When the listener is set on a view, touching that view and dragging
1065     * outside of its bounds will open the popup window. Lifting will select the
1066     * currently touched list item.
1067     * <p>
1068     * Example usage:
1069     * <pre>
1070     * ListPopupWindow myPopup = new ListPopupWindow(context);
1071     * myPopup.setAnchor(myAnchor);
1072     * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
1073     * myAnchor.setOnTouchListener(dragListener);
1074     * </pre>
1075     *
1076     * @param src the view on which the resulting listener will be set
1077     * @return a touch listener that controls drag-to-open behavior
1078     */
1079    public OnTouchListener createDragToOpenListener(View src) {
1080        return new ForwardingListener(src) {
1081            @Override
1082            public ShowableListMenu getPopup() {
1083                return ListPopupWindow.this;
1084            }
1085        };
1086    }
1087
1088    /**
1089     * <p>Builds the popup window's content and returns the height the popup
1090     * should have. Returns -1 when the content already exists.</p>
1091     *
1092     * @return the content's height or -1 if content already exists
1093     */
1094    private int buildDropDown() {
1095        ViewGroup dropDownView;
1096        int otherHeights = 0;
1097
1098        if (mDropDownList == null) {
1099            Context context = mContext;
1100
1101            /**
1102             * This Runnable exists for the sole purpose of checking if the view layout has got
1103             * completed and if so call showDropDown to display the drop down. This is used to show
1104             * the drop down as soon as possible after user opens up the search dialog, without
1105             * waiting for the normal UI pipeline to do it's job which is slower than this method.
1106             */
1107            mShowDropDownRunnable = new Runnable() {
1108                public void run() {
1109                    // View layout should be all done before displaying the drop down.
1110                    View view = getAnchorView();
1111                    if (view != null && view.getWindowToken() != null) {
1112                        show();
1113                    }
1114                }
1115            };
1116
1117            mDropDownList = createDropDownListView(context, !mModal);
1118            if (mDropDownListHighlight != null) {
1119                mDropDownList.setSelector(mDropDownListHighlight);
1120            }
1121            mDropDownList.setAdapter(mAdapter);
1122            mDropDownList.setOnItemClickListener(mItemClickListener);
1123            mDropDownList.setFocusable(true);
1124            mDropDownList.setFocusableInTouchMode(true);
1125            mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
1126                public void onItemSelected(AdapterView<?> parent, View view,
1127                        int position, long id) {
1128
1129                    if (position != -1) {
1130                        DropDownListView dropDownList = mDropDownList;
1131
1132                        if (dropDownList != null) {
1133                            dropDownList.setListSelectionHidden(false);
1134                        }
1135                    }
1136                }
1137
1138                public void onNothingSelected(AdapterView<?> parent) {
1139                }
1140            });
1141            mDropDownList.setOnScrollListener(mScrollListener);
1142
1143            if (mItemSelectedListener != null) {
1144                mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
1145            }
1146
1147            dropDownView = mDropDownList;
1148
1149            View hintView = mPromptView;
1150            if (hintView != null) {
1151                // if a hint has been specified, we accomodate more space for it and
1152                // add a text view in the drop down menu, at the bottom of the list
1153                LinearLayout hintContainer = new LinearLayout(context);
1154                hintContainer.setOrientation(LinearLayout.VERTICAL);
1155
1156                LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
1157                        ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
1158                );
1159
1160                switch (mPromptPosition) {
1161                case POSITION_PROMPT_BELOW:
1162                    hintContainer.addView(dropDownView, hintParams);
1163                    hintContainer.addView(hintView);
1164                    break;
1165
1166                case POSITION_PROMPT_ABOVE:
1167                    hintContainer.addView(hintView);
1168                    hintContainer.addView(dropDownView, hintParams);
1169                    break;
1170
1171                default:
1172                    Log.e(TAG, "Invalid hint position " + mPromptPosition);
1173                    break;
1174                }
1175
1176                // Measure the hint's height to find how much more vertical
1177                // space we need to add to the drop down's height.
1178                final int widthSize;
1179                final int widthMode;
1180                if (mDropDownWidth >= 0) {
1181                    widthMode = MeasureSpec.AT_MOST;
1182                    widthSize = mDropDownWidth;
1183                } else {
1184                    widthMode = MeasureSpec.UNSPECIFIED;
1185                    widthSize = 0;
1186                }
1187                final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
1188                final int heightSpec = MeasureSpec.UNSPECIFIED;
1189                hintView.measure(widthSpec, heightSpec);
1190
1191                hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
1192                otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
1193                        + hintParams.bottomMargin;
1194
1195                dropDownView = hintContainer;
1196            }
1197
1198            mPopup.setContentView(dropDownView);
1199        } else {
1200            final View view = mPromptView;
1201            if (view != null) {
1202                LinearLayout.LayoutParams hintParams =
1203                        (LinearLayout.LayoutParams) view.getLayoutParams();
1204                otherHeights = view.getMeasuredHeight() + hintParams.topMargin
1205                        + hintParams.bottomMargin;
1206            }
1207        }
1208
1209        // getMaxAvailableHeight() subtracts the padding, so we put it back
1210        // to get the available height for the whole window.
1211        final int padding;
1212        final Drawable background = mPopup.getBackground();
1213        if (background != null) {
1214            background.getPadding(mTempRect);
1215            padding = mTempRect.top + mTempRect.bottom;
1216
1217            // If we don't have an explicit vertical offset, determine one from
1218            // the window background so that content will line up.
1219            if (!mDropDownVerticalOffsetSet) {
1220                mDropDownVerticalOffset = -mTempRect.top;
1221            }
1222        } else {
1223            mTempRect.setEmpty();
1224            padding = 0;
1225        }
1226
1227        // Max height available on the screen for a popup.
1228        final boolean ignoreBottomDecorations =
1229                mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1230        final int maxHeight = mPopup.getMaxAvailableHeight(
1231                getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
1232        if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1233            return maxHeight + padding;
1234        }
1235
1236        final int childWidthSpec;
1237        switch (mDropDownWidth) {
1238            case ViewGroup.LayoutParams.WRAP_CONTENT:
1239                childWidthSpec = MeasureSpec.makeMeasureSpec(
1240                        mContext.getResources().getDisplayMetrics().widthPixels
1241                                - (mTempRect.left + mTempRect.right),
1242                        MeasureSpec.AT_MOST);
1243                break;
1244            case ViewGroup.LayoutParams.MATCH_PARENT:
1245                childWidthSpec = MeasureSpec.makeMeasureSpec(
1246                        mContext.getResources().getDisplayMetrics().widthPixels
1247                                - (mTempRect.left + mTempRect.right),
1248                        MeasureSpec.EXACTLY);
1249                break;
1250            default:
1251                childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
1252                break;
1253        }
1254
1255        // Add padding only if the list has items in it, that way we don't show
1256        // the popup if it is not needed.
1257        final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec,
1258                0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1);
1259        if (listContent > 0) {
1260            final int listPadding = mDropDownList.getPaddingTop()
1261                    + mDropDownList.getPaddingBottom();
1262            otherHeights += padding + listPadding;
1263        }
1264
1265        return listContent + otherHeights;
1266    }
1267
1268    /**
1269     * @hide
1270     */
1271    public void setOverlapAnchor(boolean overlap) {
1272        mOverlapAnchorSet = true;
1273        mOverlapAnchor = overlap;
1274    }
1275
1276    private class PopupDataSetObserver extends DataSetObserver {
1277        @Override
1278        public void onChanged() {
1279            if (isShowing()) {
1280                // Resize the popup to fit new content
1281                show();
1282            }
1283        }
1284
1285        @Override
1286        public void onInvalidated() {
1287            dismiss();
1288        }
1289    }
1290
1291    private class ListSelectorHider implements Runnable {
1292        public void run() {
1293            clearListSelection();
1294        }
1295    }
1296
1297    private class ResizePopupRunnable implements Runnable {
1298        public void run() {
1299            if (mDropDownList != null && mDropDownList.isAttachedToWindow()
1300                    && mDropDownList.getCount() > mDropDownList.getChildCount()
1301                    && mDropDownList.getChildCount() <= mListItemExpandMaximum) {
1302                mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1303                show();
1304            }
1305        }
1306    }
1307
1308    private class PopupTouchInterceptor implements OnTouchListener {
1309        public boolean onTouch(View v, MotionEvent event) {
1310            final int action = event.getAction();
1311            final int x = (int) event.getX();
1312            final int y = (int) event.getY();
1313
1314            if (action == MotionEvent.ACTION_DOWN &&
1315                    mPopup != null && mPopup.isShowing() &&
1316                    (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
1317                mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
1318            } else if (action == MotionEvent.ACTION_UP) {
1319                mHandler.removeCallbacks(mResizePopupRunnable);
1320            }
1321            return false;
1322        }
1323    }
1324
1325    private class PopupScrollListener implements ListView.OnScrollListener {
1326        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1327                int totalItemCount) {
1328
1329        }
1330
1331        public void onScrollStateChanged(AbsListView view, int scrollState) {
1332            if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
1333                    !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
1334                mHandler.removeCallbacks(mResizePopupRunnable);
1335                mResizePopupRunnable.run();
1336            }
1337        }
1338    }
1339}
1340