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