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