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