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