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