ListPopupWindow.java revision b40e61b77c7109c0b4e50167184e9a64cb20018e
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            final int widthSpec;
605            if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
606                // The call to PopupWindow's update method below can accept -1 for any
607                // value you do not want to update.
608                widthSpec = -1;
609            } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
610                widthSpec = getAnchorView().getWidth();
611            } else {
612                widthSpec = mDropDownWidth;
613            }
614
615            final int heightSpec;
616            if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
617                // The call to PopupWindow's update method below can accept -1 for any
618                // value you do not want to update.
619                heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
620                if (noInputMethod) {
621                    mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
622                            ViewGroup.LayoutParams.MATCH_PARENT : 0);
623                    mPopup.setHeight(0);
624                } else {
625                    mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
626                                    ViewGroup.LayoutParams.MATCH_PARENT : 0);
627                    mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
628                }
629            } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
630                heightSpec = height;
631            } else {
632                heightSpec = mDropDownHeight;
633            }
634
635            mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
636
637            mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
638                            mDropDownVerticalOffset, (widthSpec < 0)? -1 : widthSpec,
639                            (heightSpec < 0)? -1 : heightSpec);
640        } else {
641            final int widthSpec;
642            if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
643                widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
644            } else {
645                if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
646                    widthSpec = getAnchorView().getWidth();
647                } else {
648                    widthSpec = mDropDownWidth;
649                }
650            }
651
652            final int heightSpec;
653            if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
654                heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
655            } else {
656                if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
657                    heightSpec = height;
658                } else {
659                    heightSpec = mDropDownHeight;
660                }
661            }
662
663            mPopup.setWidth(widthSpec);
664            mPopup.setHeight(heightSpec);
665            mPopup.setClipToScreenEnabled(true);
666
667            // use outside touchable to dismiss drop down when touching outside of it, so
668            // only set this if the dropdown is not always visible
669            mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
670            mPopup.setTouchInterceptor(mTouchInterceptor);
671            mPopup.setEpicenterBounds(mEpicenterBounds);
672            mPopup.setOverlapAnchor(mOverlapAnchor);
673            mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset,
674                    mDropDownVerticalOffset, mDropDownGravity);
675            mDropDownList.setSelection(ListView.INVALID_POSITION);
676
677            if (!mModal || mDropDownList.isInTouchMode()) {
678                clearListSelection();
679            }
680            if (!mModal) {
681                mHandler.post(mHideSelector);
682            }
683        }
684    }
685
686    /**
687     * Dismiss the popup window.
688     */
689    @Override
690    public void dismiss() {
691        mPopup.dismiss();
692        removePromptView();
693        mPopup.setContentView(null);
694        mDropDownList = null;
695        mHandler.removeCallbacks(mResizePopupRunnable);
696    }
697
698    /**
699     * Set a listener to receive a callback when the popup is dismissed.
700     *
701     * @param listener Listener that will be notified when the popup is dismissed.
702     */
703    public void setOnDismissListener(@Nullable PopupWindow.OnDismissListener listener) {
704        mPopup.setOnDismissListener(listener);
705    }
706
707    private void removePromptView() {
708        if (mPromptView != null) {
709            final ViewParent parent = mPromptView.getParent();
710            if (parent instanceof ViewGroup) {
711                final ViewGroup group = (ViewGroup) parent;
712                group.removeView(mPromptView);
713            }
714        }
715    }
716
717    /**
718     * Control how the popup operates with an input method: one of
719     * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
720     * or {@link #INPUT_METHOD_NOT_NEEDED}.
721     *
722     * <p>If the popup is showing, calling this method will take effect only
723     * the next time the popup is shown or through a manual call to the {@link #show()}
724     * method.</p>
725     *
726     * @see #getInputMethodMode()
727     * @see #show()
728     */
729    public void setInputMethodMode(int mode) {
730        mPopup.setInputMethodMode(mode);
731    }
732
733    /**
734     * Return the current value in {@link #setInputMethodMode(int)}.
735     *
736     * @see #setInputMethodMode(int)
737     */
738    public int getInputMethodMode() {
739        return mPopup.getInputMethodMode();
740    }
741
742    /**
743     * Set the selected position of the list.
744     * Only valid when {@link #isShowing()} == {@code true}.
745     *
746     * @param position List position to set as selected.
747     */
748    public void setSelection(int position) {
749        DropDownListView list = mDropDownList;
750        if (isShowing() && list != null) {
751            list.setListSelectionHidden(false);
752            list.setSelection(position);
753            if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
754                list.setItemChecked(position, true);
755            }
756        }
757    }
758
759    /**
760     * Clear any current list selection.
761     * Only valid when {@link #isShowing()} == {@code true}.
762     */
763    public void clearListSelection() {
764        final DropDownListView list = mDropDownList;
765        if (list != null) {
766            // WARNING: Please read the comment where mListSelectionHidden is declared
767            list.setListSelectionHidden(true);
768            list.hideSelector();
769            list.requestLayout();
770        }
771    }
772
773    /**
774     * @return {@code true} if the popup is currently showing, {@code false} otherwise.
775     */
776    @Override
777    public boolean isShowing() {
778        return mPopup.isShowing();
779    }
780
781    /**
782     * @return {@code true} if this popup is configured to assume the user does not need
783     * to interact with the IME while it is showing, {@code false} otherwise.
784     */
785    public boolean isInputMethodNotNeeded() {
786        return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
787    }
788
789    /**
790     * Perform an item click operation on the specified list adapter position.
791     *
792     * @param position Adapter position for performing the click
793     * @return true if the click action could be performed, false if not.
794     *         (e.g. if the popup was not showing, this method would return false.)
795     */
796    public boolean performItemClick(int position) {
797        if (isShowing()) {
798            if (mItemClickListener != null) {
799                final DropDownListView list = mDropDownList;
800                final View child = list.getChildAt(position - list.getFirstVisiblePosition());
801                final ListAdapter adapter = list.getAdapter();
802                mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
803            }
804            return true;
805        }
806        return false;
807    }
808
809    /**
810     * @return The currently selected item or null if the popup is not showing.
811     */
812    public @Nullable Object getSelectedItem() {
813        if (!isShowing()) {
814            return null;
815        }
816        return mDropDownList.getSelectedItem();
817    }
818
819    /**
820     * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
821     * if {@link #isShowing()} == {@code false}.
822     *
823     * @see ListView#getSelectedItemPosition()
824     */
825    public int getSelectedItemPosition() {
826        if (!isShowing()) {
827            return ListView.INVALID_POSITION;
828        }
829        return mDropDownList.getSelectedItemPosition();
830    }
831
832    /**
833     * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
834     * if {@link #isShowing()} == {@code false}.
835     *
836     * @see ListView#getSelectedItemId()
837     */
838    public long getSelectedItemId() {
839        if (!isShowing()) {
840            return ListView.INVALID_ROW_ID;
841        }
842        return mDropDownList.getSelectedItemId();
843    }
844
845    /**
846     * @return The View for the currently selected item or null if
847     * {@link #isShowing()} == {@code false}.
848     *
849     * @see ListView#getSelectedView()
850     */
851    public @Nullable View getSelectedView() {
852        if (!isShowing()) {
853            return null;
854        }
855        return mDropDownList.getSelectedView();
856    }
857
858    /**
859     * @return The {@link ListView} displayed within the popup window.
860     * Only valid when {@link #isShowing()} == {@code true}.
861     */
862    @Override
863    public @Nullable ListView getListView() {
864        return mDropDownList;
865    }
866
867    @NonNull DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
868        return new DropDownListView(context, hijackFocus);
869    }
870
871    /**
872     * The maximum number of list items that can be visible and still have
873     * the list expand when touched.
874     *
875     * @param max Max number of items that can be visible and still allow the list to expand.
876     */
877    void setListItemExpandMax(int max) {
878        mListItemExpandMaximum = max;
879    }
880
881    /**
882     * Filter key down events. By forwarding key down events to this function,
883     * views using non-modal ListPopupWindow can have it handle key selection of items.
884     *
885     * @param keyCode keyCode param passed to the host view's onKeyDown
886     * @param event event param passed to the host view's onKeyDown
887     * @return true if the event was handled, false if it was ignored.
888     *
889     * @see #setModal(boolean)
890     * @see #onKeyUp(int, KeyEvent)
891     */
892    public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
893        // when the drop down is shown, we drive it directly
894        if (isShowing()) {
895            // the key events are forwarded to the list in the drop down view
896            // note that ListView handles space but we don't want that to happen
897            // also if selection is not currently in the drop down, then don't
898            // let center or enter presses go there since that would cause it
899            // to select one of its items
900            if (keyCode != KeyEvent.KEYCODE_SPACE
901                    && (mDropDownList.getSelectedItemPosition() >= 0
902                            || !KeyEvent.isConfirmKey(keyCode))) {
903                int curIndex = mDropDownList.getSelectedItemPosition();
904                boolean consumed;
905
906                final boolean below = !mPopup.isAboveAnchor();
907
908                final ListAdapter adapter = mAdapter;
909
910                boolean allEnabled;
911                int firstItem = Integer.MAX_VALUE;
912                int lastItem = Integer.MIN_VALUE;
913
914                if (adapter != null) {
915                    allEnabled = adapter.areAllItemsEnabled();
916                    firstItem = allEnabled ? 0 :
917                            mDropDownList.lookForSelectablePosition(0, true);
918                    lastItem = allEnabled ? adapter.getCount() - 1 :
919                            mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
920                }
921
922                if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
923                        (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
924                    // When the selection is at the top, we block the key
925                    // event to prevent focus from moving.
926                    clearListSelection();
927                    mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
928                    show();
929                    return true;
930                } else {
931                    // WARNING: Please read the comment where mListSelectionHidden
932                    //          is declared
933                    mDropDownList.setListSelectionHidden(false);
934                }
935
936                consumed = mDropDownList.onKeyDown(keyCode, event);
937                if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
938
939                if (consumed) {
940                    // If it handled the key event, then the user is
941                    // navigating in the list, so we should put it in front.
942                    mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
943                    // Here's a little trick we need to do to make sure that
944                    // the list view is actually showing its focus indicator,
945                    // by ensuring it has focus and getting its window out
946                    // of touch mode.
947                    mDropDownList.requestFocusFromTouch();
948                    show();
949
950                    switch (keyCode) {
951                        // avoid passing the focus from the text view to the
952                        // next component
953                        case KeyEvent.KEYCODE_ENTER:
954                        case KeyEvent.KEYCODE_DPAD_CENTER:
955                        case KeyEvent.KEYCODE_DPAD_DOWN:
956                        case KeyEvent.KEYCODE_DPAD_UP:
957                            return true;
958                    }
959                } else {
960                    if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
961                        // when the selection is at the bottom, we block the
962                        // event to avoid going to the next focusable widget
963                        if (curIndex == lastItem) {
964                            return true;
965                        }
966                    } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
967                            curIndex == firstItem) {
968                        return true;
969                    }
970                }
971            }
972        }
973
974        return false;
975    }
976
977    /**
978     * Filter key up events. By forwarding key up events to this function,
979     * views using non-modal ListPopupWindow can have it handle key selection of items.
980     *
981     * @param keyCode keyCode param passed to the host view's onKeyUp
982     * @param event event param passed to the host view's onKeyUp
983     * @return true if the event was handled, false if it was ignored.
984     *
985     * @see #setModal(boolean)
986     * @see #onKeyDown(int, KeyEvent)
987     */
988    public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
989        if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
990            boolean consumed = mDropDownList.onKeyUp(keyCode, event);
991            if (consumed && KeyEvent.isConfirmKey(keyCode)) {
992                // if the list accepts the key events and the key event was a click, the text view
993                // gets the selected item from the drop down as its content
994                dismiss();
995            }
996            return consumed;
997        }
998        return false;
999    }
1000
1001    /**
1002     * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
1003     * events to this function, views using ListPopupWindow can have it dismiss the popup
1004     * when the back key is pressed.
1005     *
1006     * @param keyCode keyCode param passed to the host view's onKeyPreIme
1007     * @param event event param passed to the host view's onKeyPreIme
1008     * @return true if the event was handled, false if it was ignored.
1009     *
1010     * @see #setModal(boolean)
1011     */
1012    public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {
1013        if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
1014            // special case for the back key, we do not even try to send it
1015            // to the drop down list but instead, consume it immediately
1016            final View anchorView = mDropDownAnchorView;
1017            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
1018                KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
1019                if (state != null) {
1020                    state.startTracking(event, this);
1021                }
1022                return true;
1023            } else if (event.getAction() == KeyEvent.ACTION_UP) {
1024                KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
1025                if (state != null) {
1026                    state.handleUpEvent(event);
1027                }
1028                if (event.isTracking() && !event.isCanceled()) {
1029                    dismiss();
1030                    return true;
1031                }
1032            }
1033        }
1034        return false;
1035    }
1036
1037    /**
1038     * Returns an {@link OnTouchListener} that can be added to the source view
1039     * to implement drag-to-open behavior. Generally, the source view should be
1040     * the same view that was passed to {@link #setAnchorView}.
1041     * <p>
1042     * When the listener is set on a view, touching that view and dragging
1043     * outside of its bounds will open the popup window. Lifting will select the
1044     * currently touched list item.
1045     * <p>
1046     * Example usage:
1047     * <pre>
1048     * ListPopupWindow myPopup = new ListPopupWindow(context);
1049     * myPopup.setAnchor(myAnchor);
1050     * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
1051     * myAnchor.setOnTouchListener(dragListener);
1052     * </pre>
1053     *
1054     * @param src the view on which the resulting listener will be set
1055     * @return a touch listener that controls drag-to-open behavior
1056     */
1057    public OnTouchListener createDragToOpenListener(View src) {
1058        return new ForwardingListener(src) {
1059            @Override
1060            public ShowableListMenu getPopup() {
1061                return ListPopupWindow.this;
1062            }
1063        };
1064    }
1065
1066    /**
1067     * <p>Builds the popup window's content and returns the height the popup
1068     * should have. Returns -1 when the content already exists.</p>
1069     *
1070     * @return the content's height or -1 if content already exists
1071     */
1072    private int buildDropDown() {
1073        ViewGroup dropDownView;
1074        int otherHeights = 0;
1075
1076        if (mDropDownList == null) {
1077            Context context = mContext;
1078
1079            /**
1080             * This Runnable exists for the sole purpose of checking if the view layout has got
1081             * completed and if so call showDropDown to display the drop down. This is used to show
1082             * the drop down as soon as possible after user opens up the search dialog, without
1083             * waiting for the normal UI pipeline to do it's job which is slower than this method.
1084             */
1085            mShowDropDownRunnable = new Runnable() {
1086                public void run() {
1087                    // View layout should be all done before displaying the drop down.
1088                    View view = getAnchorView();
1089                    if (view != null && view.getWindowToken() != null) {
1090                        show();
1091                    }
1092                }
1093            };
1094
1095            mDropDownList = createDropDownListView(context, !mModal);
1096            if (mDropDownListHighlight != null) {
1097                mDropDownList.setSelector(mDropDownListHighlight);
1098            }
1099            mDropDownList.setAdapter(mAdapter);
1100            mDropDownList.setOnItemClickListener(mItemClickListener);
1101            mDropDownList.setFocusable(true);
1102            mDropDownList.setFocusableInTouchMode(true);
1103            mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
1104                public void onItemSelected(AdapterView<?> parent, View view,
1105                        int position, long id) {
1106
1107                    if (position != -1) {
1108                        DropDownListView dropDownList = mDropDownList;
1109
1110                        if (dropDownList != null) {
1111                            dropDownList.setListSelectionHidden(false);
1112                        }
1113                    }
1114                }
1115
1116                public void onNothingSelected(AdapterView<?> parent) {
1117                }
1118            });
1119            mDropDownList.setOnScrollListener(mScrollListener);
1120
1121            if (mItemSelectedListener != null) {
1122                mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
1123            }
1124
1125            dropDownView = mDropDownList;
1126
1127            View hintView = mPromptView;
1128            if (hintView != null) {
1129                // if a hint has been specified, we accomodate more space for it and
1130                // add a text view in the drop down menu, at the bottom of the list
1131                LinearLayout hintContainer = new LinearLayout(context);
1132                hintContainer.setOrientation(LinearLayout.VERTICAL);
1133
1134                LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
1135                        ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
1136                );
1137
1138                switch (mPromptPosition) {
1139                case POSITION_PROMPT_BELOW:
1140                    hintContainer.addView(dropDownView, hintParams);
1141                    hintContainer.addView(hintView);
1142                    break;
1143
1144                case POSITION_PROMPT_ABOVE:
1145                    hintContainer.addView(hintView);
1146                    hintContainer.addView(dropDownView, hintParams);
1147                    break;
1148
1149                default:
1150                    Log.e(TAG, "Invalid hint position " + mPromptPosition);
1151                    break;
1152                }
1153
1154                // Measure the hint's height to find how much more vertical
1155                // space we need to add to the drop down's height.
1156                final int widthSize;
1157                final int widthMode;
1158                if (mDropDownWidth >= 0) {
1159                    widthMode = MeasureSpec.AT_MOST;
1160                    widthSize = mDropDownWidth;
1161                } else {
1162                    widthMode = MeasureSpec.UNSPECIFIED;
1163                    widthSize = 0;
1164                }
1165                final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
1166                final int heightSpec = MeasureSpec.UNSPECIFIED;
1167                hintView.measure(widthSpec, heightSpec);
1168
1169                hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
1170                otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
1171                        + hintParams.bottomMargin;
1172
1173                dropDownView = hintContainer;
1174            }
1175
1176            mPopup.setContentView(dropDownView);
1177        } else {
1178            final View view = mPromptView;
1179            if (view != null) {
1180                LinearLayout.LayoutParams hintParams =
1181                        (LinearLayout.LayoutParams) view.getLayoutParams();
1182                otherHeights = view.getMeasuredHeight() + hintParams.topMargin
1183                        + hintParams.bottomMargin;
1184            }
1185        }
1186
1187        // getMaxAvailableHeight() subtracts the padding, so we put it back
1188        // to get the available height for the whole window.
1189        final int padding;
1190        final Drawable background = mPopup.getBackground();
1191        if (background != null) {
1192            background.getPadding(mTempRect);
1193            padding = mTempRect.top + mTempRect.bottom;
1194
1195            // If we don't have an explicit vertical offset, determine one from
1196            // the window background so that content will line up.
1197            if (!mDropDownVerticalOffsetSet) {
1198                mDropDownVerticalOffset = -mTempRect.top;
1199            }
1200        } else {
1201            mTempRect.setEmpty();
1202            padding = 0;
1203        }
1204
1205        // Max height available on the screen for a popup.
1206        final boolean ignoreBottomDecorations =
1207                mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1208        final int maxHeight = mPopup.getMaxAvailableHeight(
1209                getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
1210        if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1211            return maxHeight + padding;
1212        }
1213
1214        final int childWidthSpec;
1215        switch (mDropDownWidth) {
1216            case ViewGroup.LayoutParams.WRAP_CONTENT:
1217                childWidthSpec = MeasureSpec.makeMeasureSpec(
1218                        mContext.getResources().getDisplayMetrics().widthPixels
1219                                - (mTempRect.left + mTempRect.right),
1220                        MeasureSpec.AT_MOST);
1221                break;
1222            case ViewGroup.LayoutParams.MATCH_PARENT:
1223                childWidthSpec = MeasureSpec.makeMeasureSpec(
1224                        mContext.getResources().getDisplayMetrics().widthPixels
1225                                - (mTempRect.left + mTempRect.right),
1226                        MeasureSpec.EXACTLY);
1227                break;
1228            default:
1229                childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
1230                break;
1231        }
1232
1233        // Add padding only if the list has items in it, that way we don't show
1234        // the popup if it is not needed.
1235        final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec,
1236                0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1);
1237        if (listContent > 0) {
1238            final int listPadding = mDropDownList.getPaddingTop()
1239                    + mDropDownList.getPaddingBottom();
1240            otherHeights += padding + listPadding;
1241        }
1242
1243        return listContent + otherHeights;
1244    }
1245
1246    /**
1247     * @hide
1248     */
1249    public void setOverlapAnchor(boolean overlap) {
1250        mOverlapAnchor = overlap;
1251    }
1252
1253    private class PopupDataSetObserver extends DataSetObserver {
1254        @Override
1255        public void onChanged() {
1256            if (isShowing()) {
1257                // Resize the popup to fit new content
1258                show();
1259            }
1260        }
1261
1262        @Override
1263        public void onInvalidated() {
1264            dismiss();
1265        }
1266    }
1267
1268    private class ListSelectorHider implements Runnable {
1269        public void run() {
1270            clearListSelection();
1271        }
1272    }
1273
1274    private class ResizePopupRunnable implements Runnable {
1275        public void run() {
1276            if (mDropDownList != null && mDropDownList.isAttachedToWindow()
1277                    && mDropDownList.getCount() > mDropDownList.getChildCount()
1278                    && mDropDownList.getChildCount() <= mListItemExpandMaximum) {
1279                mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1280                show();
1281            }
1282        }
1283    }
1284
1285    private class PopupTouchInterceptor implements OnTouchListener {
1286        public boolean onTouch(View v, MotionEvent event) {
1287            final int action = event.getAction();
1288            final int x = (int) event.getX();
1289            final int y = (int) event.getY();
1290
1291            if (action == MotionEvent.ACTION_DOWN &&
1292                    mPopup != null && mPopup.isShowing() &&
1293                    (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
1294                mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
1295            } else if (action == MotionEvent.ACTION_UP) {
1296                mHandler.removeCallbacks(mResizePopupRunnable);
1297            }
1298            return false;
1299        }
1300    }
1301
1302    private class PopupScrollListener implements ListView.OnScrollListener {
1303        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1304                int totalItemCount) {
1305
1306        }
1307
1308        public void onScrollStateChanged(AbsListView view, int scrollState) {
1309            if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
1310                    !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
1311                mHandler.removeCallbacks(mResizePopupRunnable);
1312                mResizePopupRunnable.run();
1313            }
1314        }
1315    }
1316}
1317