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