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