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