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