1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import static android.view.flags.Flags.enableArrowIconOnHoverWhenClickable;
20 import static android.view.flags.Flags.FLAG_ENABLE_ARROW_ICON_ON_HOVER_WHEN_CLICKABLE;
21 
22 import android.annotation.DrawableRes;
23 import android.annotation.FlaggedApi;
24 import android.annotation.Nullable;
25 import android.annotation.TestApi;
26 import android.annotation.Widget;
27 import android.app.AlertDialog;
28 import android.compat.annotation.UnsupportedAppUsage;
29 import android.content.Context;
30 import android.content.DialogInterface;
31 import android.content.DialogInterface.OnClickListener;
32 import android.content.res.Resources;
33 import android.content.res.Resources.Theme;
34 import android.content.res.TypedArray;
35 import android.database.DataSetObserver;
36 import android.graphics.Rect;
37 import android.graphics.drawable.Drawable;
38 import android.os.Build;
39 import android.os.Parcel;
40 import android.os.Parcelable;
41 import android.util.AttributeSet;
42 import android.util.Log;
43 import android.view.ContextThemeWrapper;
44 import android.view.Gravity;
45 import android.view.InputDevice;
46 import android.view.MotionEvent;
47 import android.view.PointerIcon;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.view.ViewTreeObserver;
51 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
52 import android.view.accessibility.AccessibilityNodeInfo;
53 import android.view.inspector.InspectableProperty;
54 import android.widget.PopupWindow.OnDismissListener;
55 
56 import com.android.internal.R;
57 import com.android.internal.view.menu.ShowableListMenu;
58 
59 /**
60  * A view that displays one child at a time and lets the user pick among them.
61  * The items in the Spinner come from the {@link Adapter} associated with
62  * this view.
63  *
64  * <p>See the <a href="{@docRoot}guide/topics/ui/controls/spinner.html">Spinners</a> guide.</p>
65  *
66  * @attr ref android.R.styleable#Spinner_dropDownSelector
67  * @attr ref android.R.styleable#Spinner_dropDownWidth
68  * @attr ref android.R.styleable#Spinner_gravity
69  * @attr ref android.R.styleable#Spinner_popupBackground
70  * @attr ref android.R.styleable#Spinner_prompt
71  * @attr ref android.R.styleable#Spinner_spinnerMode
72  * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
73  * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
74  */
75 @Widget
76 public class Spinner extends AbsSpinner implements OnClickListener {
77     private static final String TAG = "Spinner";
78 
79     // Only measure this many items to get a decent max width.
80     private static final int MAX_ITEMS_MEASURED = 15;
81 
82     /**
83      * Use a dialog window for selecting spinner options.
84      */
85     public static final int MODE_DIALOG = 0;
86 
87     /**
88      * Use a dropdown anchored to the Spinner for selecting spinner options.
89      */
90     public static final int MODE_DROPDOWN = 1;
91 
92     /**
93      * Use the theme-supplied value to select the dropdown mode.
94      */
95     private static final int MODE_THEME = -1;
96 
97     private final Rect mTempRect = new Rect();
98 
99     /** Context used to inflate the popup window or dialog. */
100     private final Context mPopupContext;
101 
102     /** Forwarding listener used to implement drag-to-open. */
103     @UnsupportedAppUsage
104     private ForwardingListener mForwardingListener;
105 
106     /** Temporary holder for setAdapter() calls from the super constructor. */
107     private SpinnerAdapter mTempAdapter;
108 
109     @UnsupportedAppUsage
110     private SpinnerPopup mPopup;
111     int mDropDownWidth;
112 
113     private int mGravity;
114     private boolean mDisableChildrenWhenDisabled;
115 
116     /**
117      * Constructs a new spinner with the given context's theme.
118      *
119      * @param context The Context the view is running in, through which it can
120      *                access the current theme, resources, etc.
121      */
Spinner(Context context)122     public Spinner(Context context) {
123         this(context, null);
124     }
125 
126     /**
127      * Constructs a new spinner with the given context's theme and the supplied
128      * mode of displaying choices. <code>mode</code> may be one of
129      * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN}.
130      *
131      * @param context The Context the view is running in, through which it can
132      *                access the current theme, resources, etc.
133      * @param mode Constant describing how the user will select choices from
134      *             the spinner.
135      *
136      * @see #MODE_DIALOG
137      * @see #MODE_DROPDOWN
138      */
Spinner(Context context, int mode)139     public Spinner(Context context, int mode) {
140         this(context, null, com.android.internal.R.attr.spinnerStyle, mode);
141     }
142 
143     /**
144      * Constructs a new spinner with the given context's theme and the supplied
145      * attribute set.
146      *
147      * @param context The Context the view is running in, through which it can
148      *                access the current theme, resources, etc.
149      * @param attrs The attributes of the XML tag that is inflating the view.
150      */
Spinner(Context context, AttributeSet attrs)151     public Spinner(Context context, AttributeSet attrs) {
152         this(context, attrs, com.android.internal.R.attr.spinnerStyle);
153     }
154 
155     /**
156      * Constructs a new spinner with the given context's theme, the supplied
157      * attribute set, and default style attribute.
158      *
159      * @param context The Context the view is running in, through which it can
160      *                access the current theme, resources, etc.
161      * @param attrs The attributes of the XML tag that is inflating the view.
162      * @param defStyleAttr An attribute in the current theme that contains a
163      *                     reference to a style resource that supplies default
164      *                     values for the view. Can be 0 to not look for
165      *                     defaults.
166      */
Spinner(Context context, AttributeSet attrs, int defStyleAttr)167     public Spinner(Context context, AttributeSet attrs, int defStyleAttr) {
168         this(context, attrs, defStyleAttr, 0, MODE_THEME);
169     }
170 
171     /**
172      * Constructs a new spinner with the given context's theme, the supplied
173      * attribute set, and default style attribute. <code>mode</code> may be one
174      * of {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN} and determines how the
175      * user will select choices from the spinner.
176      *
177      * @param context The Context the view is running in, through which it can
178      *                access the current theme, resources, etc.
179      * @param attrs The attributes of the XML tag that is inflating the view.
180      * @param defStyleAttr An attribute in the current theme that contains a
181      *                     reference to a style resource that supplies default
182      *                     values for the view. Can be 0 to not look for defaults.
183      * @param mode Constant describing how the user will select choices from the
184      *             spinner.
185      *
186      * @see #MODE_DIALOG
187      * @see #MODE_DROPDOWN
188      */
Spinner(Context context, AttributeSet attrs, int defStyleAttr, int mode)189     public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
190         this(context, attrs, defStyleAttr, 0, mode);
191     }
192 
193     /**
194      * Constructs a new spinner with the given context's theme, the supplied
195      * attribute set, and default styles. <code>mode</code> may be one of
196      * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN} and determines how the
197      * user will select choices from the spinner.
198      *
199      * @param context The Context the view is running in, through which it can
200      *                access the current theme, resources, etc.
201      * @param attrs The attributes of the XML tag that is inflating the view.
202      * @param defStyleAttr An attribute in the current theme that contains a
203      *                     reference to a style resource that supplies default
204      *                     values for the view. Can be 0 to not look for
205      *                     defaults.
206      * @param defStyleRes A resource identifier of a style resource that
207      *                    supplies default values for the view, used only if
208      *                    defStyleAttr is 0 or can not be found in the theme.
209      *                    Can be 0 to not look for defaults.
210      * @param mode Constant describing how the user will select choices from
211      *             the spinner.
212      *
213      * @see #MODE_DIALOG
214      * @see #MODE_DROPDOWN
215      */
Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode)216     public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
217             int mode) {
218         this(context, attrs, defStyleAttr, defStyleRes, mode, null);
219     }
220 
221     /**
222      * Constructs a new spinner with the given context, the supplied attribute
223      * set, default styles, popup mode (one of {@link #MODE_DIALOG} or
224      * {@link #MODE_DROPDOWN}), and the theme against which the popup should be
225      * inflated.
226      *
227      * @param context The context against which the view is inflated, which
228      *                provides access to the current theme, resources, etc.
229      * @param attrs The attributes of the XML tag that is inflating the view.
230      * @param defStyleAttr An attribute in the current theme that contains a
231      *                     reference to a style resource that supplies default
232      *                     values for the view. Can be 0 to not look for
233      *                     defaults.
234      * @param defStyleRes A resource identifier of a style resource that
235      *                    supplies default values for the view, used only if
236      *                    defStyleAttr is 0 or can not be found in the theme.
237      *                    Can be 0 to not look for defaults.
238      * @param mode Constant describing how the user will select choices from
239      *             the spinner.
240      * @param popupTheme The theme against which the dialog or dropdown popup
241      *                   should be inflated. May be {@code null} to use the
242      *                   view theme. If set, this will override any value
243      *                   specified by
244      *                   {@link android.R.styleable#Spinner_popupTheme}.
245      *
246      * @see #MODE_DIALOG
247      * @see #MODE_DROPDOWN
248      */
Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode, Theme popupTheme)249     public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode,
250             Theme popupTheme) {
251         super(context, attrs, defStyleAttr, defStyleRes);
252 
253         final TypedArray a = context.obtainStyledAttributes(
254                 attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
255         saveAttributeDataForStyleable(context, R.styleable.Spinner,
256                 attrs, a, defStyleAttr, defStyleRes);
257 
258         if (popupTheme != null) {
259             mPopupContext = new ContextThemeWrapper(context, popupTheme);
260         } else {
261             final int popupThemeResId = a.getResourceId(R.styleable.Spinner_popupTheme, 0);
262             if (popupThemeResId != 0) {
263                 mPopupContext = new ContextThemeWrapper(context, popupThemeResId);
264             } else {
265                 mPopupContext = context;
266             }
267         }
268 
269         if (mode == MODE_THEME) {
270             mode = a.getInt(R.styleable.Spinner_spinnerMode, MODE_DIALOG);
271         }
272 
273         switch (mode) {
274             case MODE_DIALOG: {
275                 mPopup = new DialogPopup();
276                 mPopup.setPromptText(a.getString(R.styleable.Spinner_prompt));
277                 break;
278             }
279 
280             case MODE_DROPDOWN: {
281                 final DropdownPopup popup = new DropdownPopup(
282                         mPopupContext, attrs, defStyleAttr, defStyleRes);
283                 final TypedArray pa = mPopupContext.obtainStyledAttributes(
284                         attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
285                 mDropDownWidth = pa.getLayoutDimension(R.styleable.Spinner_dropDownWidth,
286                         ViewGroup.LayoutParams.WRAP_CONTENT);
287                 if (pa.hasValueOrEmpty(R.styleable.Spinner_dropDownSelector)) {
288                     popup.setListSelector(pa.getDrawable(
289                             R.styleable.Spinner_dropDownSelector));
290                 }
291                 popup.setBackgroundDrawable(pa.getDrawable(R.styleable.Spinner_popupBackground));
292                 popup.setPromptText(a.getString(R.styleable.Spinner_prompt));
293                 pa.recycle();
294 
295                 mPopup = popup;
296                 mForwardingListener = new ForwardingListener(this) {
297                     @Override
298                     public ShowableListMenu getPopup() {
299                         return popup;
300                     }
301 
302                     @Override
303                     public boolean onForwardingStarted() {
304                         if (!mPopup.isShowing()) {
305                             mPopup.show(getTextDirection(), getTextAlignment());
306                         }
307                         return true;
308                     }
309                 };
310                 break;
311             }
312         }
313 
314         mGravity = a.getInt(R.styleable.Spinner_gravity, Gravity.CENTER);
315         mDisableChildrenWhenDisabled = a.getBoolean(
316                 R.styleable.Spinner_disableChildrenWhenDisabled, false);
317 
318         a.recycle();
319 
320         // Base constructor can call setAdapter before we initialize mPopup.
321         // Finish setting things up if this happened.
322         if (mTempAdapter != null) {
323             setAdapter(mTempAdapter);
324             mTempAdapter = null;
325         }
326     }
327 
328     /**
329      * @return the context used to inflate the Spinner's popup or dialog window
330      */
getPopupContext()331     public Context getPopupContext() {
332         return mPopupContext;
333     }
334 
335     /**
336      * Set the background drawable for the spinner's popup window of choices.
337      * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
338      *
339      * @param background Background drawable
340      *
341      * @attr ref android.R.styleable#Spinner_popupBackground
342      */
setPopupBackgroundDrawable(Drawable background)343     public void setPopupBackgroundDrawable(Drawable background) {
344         if (!(mPopup instanceof DropdownPopup)) {
345             Log.e(TAG, "setPopupBackgroundDrawable: incompatible spinner mode; ignoring...");
346             return;
347         }
348         mPopup.setBackgroundDrawable(background);
349     }
350 
351     /**
352      * Set the background drawable for the spinner's popup window of choices.
353      * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
354      *
355      * @param resId Resource ID of a background drawable
356      *
357      * @attr ref android.R.styleable#Spinner_popupBackground
358      */
setPopupBackgroundResource(@rawableRes int resId)359     public void setPopupBackgroundResource(@DrawableRes int resId) {
360         setPopupBackgroundDrawable(getPopupContext().getDrawable(resId));
361     }
362 
363     /**
364      * Get the background drawable for the spinner's popup window of choices.
365      * Only valid in {@link #MODE_DROPDOWN}; other modes will return null.
366      *
367      * @return background Background drawable
368      *
369      * @attr ref android.R.styleable#Spinner_popupBackground
370      */
371     @InspectableProperty
getPopupBackground()372     public Drawable getPopupBackground() {
373         return mPopup.getBackground();
374     }
375 
376     /**
377      * @hide
378      */
379     @TestApi
isPopupShowing()380     public boolean isPopupShowing() {
381         return (mPopup != null) && mPopup.isShowing();
382     }
383 
384     /**
385      * Set a vertical offset in pixels for the spinner's popup window of choices.
386      * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
387      *
388      * @param pixels Vertical offset in pixels
389      *
390      * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
391      */
setDropDownVerticalOffset(int pixels)392     public void setDropDownVerticalOffset(int pixels) {
393         mPopup.setVerticalOffset(pixels);
394     }
395 
396     /**
397      * Get the configured vertical offset in pixels for the spinner's popup window of choices.
398      * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
399      *
400      * @return Vertical offset in pixels
401      *
402      * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
403      */
404     @InspectableProperty
getDropDownVerticalOffset()405     public int getDropDownVerticalOffset() {
406         return mPopup.getVerticalOffset();
407     }
408 
409     /**
410      * Set a horizontal offset in pixels for the spinner's popup window of choices.
411      * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
412      *
413      * @param pixels Horizontal offset in pixels
414      *
415      * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
416      */
setDropDownHorizontalOffset(int pixels)417     public void setDropDownHorizontalOffset(int pixels) {
418         mPopup.setHorizontalOffset(pixels);
419     }
420 
421     /**
422      * Get the configured horizontal offset in pixels for the spinner's popup window of choices.
423      * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
424      *
425      * @return Horizontal offset in pixels
426      *
427      * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
428      */
429     @InspectableProperty
getDropDownHorizontalOffset()430     public int getDropDownHorizontalOffset() {
431         return mPopup.getHorizontalOffset();
432     }
433 
434     /**
435      * Set the width of the spinner's popup window of choices in pixels. This value
436      * may also be set to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
437      * to match the width of the Spinner itself, or
438      * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
439      * of contained dropdown list items.
440      *
441      * <p>Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.</p>
442      *
443      * @param pixels Width in pixels, WRAP_CONTENT, or MATCH_PARENT
444      *
445      * @attr ref android.R.styleable#Spinner_dropDownWidth
446      */
setDropDownWidth(int pixels)447     public void setDropDownWidth(int pixels) {
448         if (!(mPopup instanceof DropdownPopup)) {
449             Log.e(TAG, "Cannot set dropdown width for MODE_DIALOG, ignoring");
450             return;
451         }
452         mDropDownWidth = pixels;
453     }
454 
455     /**
456      * Get the configured width of the spinner's popup window of choices in pixels.
457      * The returned value may also be {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
458      * meaning the popup window will match the width of the Spinner itself, or
459      * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
460      * of contained dropdown list items.
461      *
462      * @return Width in pixels, WRAP_CONTENT, or MATCH_PARENT
463      *
464      * @attr ref android.R.styleable#Spinner_dropDownWidth
465      */
466     @InspectableProperty
getDropDownWidth()467     public int getDropDownWidth() {
468         return mDropDownWidth;
469     }
470 
471     @Override
setEnabled(boolean enabled)472     public void setEnabled(boolean enabled) {
473         super.setEnabled(enabled);
474         if (mDisableChildrenWhenDisabled) {
475             final int count = getChildCount();
476             for (int i = 0; i < count; i++) {
477                 getChildAt(i).setEnabled(enabled);
478             }
479         }
480     }
481 
482     /**
483      * Describes how the selected item view is positioned. Currently only the horizontal component
484      * is used. The default is determined by the current theme.
485      *
486      * @param gravity See {@link android.view.Gravity}
487      *
488      * @attr ref android.R.styleable#Spinner_gravity
489      */
setGravity(int gravity)490     public void setGravity(int gravity) {
491         if (mGravity != gravity) {
492             if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
493                 gravity |= Gravity.START;
494             }
495             mGravity = gravity;
496             requestLayout();
497         }
498     }
499 
500     /**
501      * Describes how the selected item view is positioned. The default is determined by the
502      * current theme.
503      *
504      * @return A {@link android.view.Gravity Gravity} value
505      */
506     @InspectableProperty(valueType = InspectableProperty.ValueType.GRAVITY)
getGravity()507     public int getGravity() {
508         return mGravity;
509     }
510 
511     /**
512      * Sets the {@link SpinnerAdapter} used to provide the data which backs
513      * this Spinner.
514      * <p>
515      * If this Spinner has a popup theme set in XML via the
516      * {@link android.R.styleable#Spinner_popupTheme popupTheme} attribute, the
517      * adapter should inflate drop-down views using the same theme. The easiest
518      * way to achieve this is by using {@link #getPopupContext()} to obtain a
519      * layout inflater for use in
520      * {@link SpinnerAdapter#getDropDownView(int, View, ViewGroup)}.
521      * <p>
522      * Spinner overrides {@link Adapter#getViewTypeCount()} on the
523      * Adapter associated with this view. Calling
524      * {@link Adapter#getItemViewType(int) getItemViewType(int)} on the object
525      * returned from {@link #getAdapter()} will always return 0. Calling
526      * {@link Adapter#getViewTypeCount() getViewTypeCount()} will always return
527      * 1. On API {@link Build.VERSION_CODES#LOLLIPOP} and above, attempting to set an
528      * adapter with more than one view type will throw an
529      * {@link IllegalArgumentException}.
530      *
531      * @param adapter the adapter to set
532      *
533      * @see AbsSpinner#setAdapter(SpinnerAdapter)
534      * @throws IllegalArgumentException if the adapter has more than one view
535      *         type
536      */
537     @Override
setAdapter(SpinnerAdapter adapter)538     public void setAdapter(SpinnerAdapter adapter) {
539         // The super constructor may call setAdapter before we're prepared.
540         // Postpone doing anything until we've finished construction.
541         if (mPopup == null) {
542             mTempAdapter = adapter;
543             return;
544         }
545 
546         super.setAdapter(adapter);
547 
548         mRecycler.clear();
549 
550         final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
551         if (targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP
552                 && adapter != null && adapter.getViewTypeCount() != 1) {
553             throw new IllegalArgumentException("Spinner adapter view type count must be 1");
554         }
555 
556         final Context popupContext = mPopupContext == null ? mContext : mPopupContext;
557         mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme()));
558     }
559 
560     @Override
getBaseline()561     public int getBaseline() {
562         View child = null;
563 
564         if (getChildCount() > 0) {
565             child = getChildAt(0);
566         } else if (mAdapter != null && mAdapter.getCount() > 0) {
567             child = makeView(0, false);
568             mRecycler.put(0, child);
569         }
570 
571         if (child != null) {
572             final int childBaseline = child.getBaseline();
573             return childBaseline >= 0 ? child.getTop() + childBaseline : -1;
574         } else {
575             return -1;
576         }
577     }
578 
579     @Override
onDetachedFromWindow()580     protected void onDetachedFromWindow() {
581         super.onDetachedFromWindow();
582 
583         if (mPopup != null && mPopup.isShowing()) {
584             mPopup.dismiss();
585         }
586     }
587 
588     /**
589      * <p>A spinner does not support item click events. Calling this method
590      * will raise an exception.</p>
591      * <p>Instead use {@link AdapterView#setOnItemSelectedListener}.
592      *
593      * @param l this listener will be ignored
594      */
595     @Override
setOnItemClickListener(OnItemClickListener l)596     public void setOnItemClickListener(OnItemClickListener l) {
597         throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
598     }
599 
600     /**
601      * @hide internal use only
602      */
603     @UnsupportedAppUsage
setOnItemClickListenerInt(OnItemClickListener l)604     public void setOnItemClickListenerInt(OnItemClickListener l) {
605         super.setOnItemClickListener(l);
606     }
607 
608     @Override
onTouchEvent(MotionEvent event)609     public boolean onTouchEvent(MotionEvent event) {
610         if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) {
611             return true;
612         }
613 
614         return super.onTouchEvent(event);
615     }
616 
617     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)618     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
619         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
620         if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
621             final int measuredWidth = getMeasuredWidth();
622             setMeasuredDimension(Math.min(Math.max(measuredWidth,
623                     measureContentWidth(getAdapter(), getBackground())),
624                     MeasureSpec.getSize(widthMeasureSpec)),
625                     getMeasuredHeight());
626         }
627     }
628 
629     /**
630      * @see android.view.View#onLayout(boolean,int,int,int,int)
631      *
632      * Creates and positions all views
633      *
634      */
635     @Override
onLayout(boolean changed, int l, int t, int r, int b)636     protected void onLayout(boolean changed, int l, int t, int r, int b) {
637         super.onLayout(changed, l, t, r, b);
638         mInLayout = true;
639         layout(0, false);
640         mInLayout = false;
641     }
642 
643     /**
644      * Creates and positions all views for this Spinner.
645      *
646      * @param delta Change in the selected position. +1 means selection is moving to the right,
647      * so views are scrolling to the left. -1 means selection is moving to the left.
648      */
649     @Override
layout(int delta, boolean animate)650     void layout(int delta, boolean animate) {
651         int childrenLeft = mSpinnerPadding.left;
652         int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
653 
654         if (mDataChanged) {
655             handleDataChanged();
656         }
657 
658         // Handle the empty set by removing all views
659         if (mItemCount == 0) {
660             resetList();
661             return;
662         }
663 
664         if (mNextSelectedPosition >= 0) {
665             setSelectedPositionInt(mNextSelectedPosition);
666         }
667 
668         recycleAllViews();
669 
670         // Clear out old views
671         removeAllViewsInLayout();
672 
673         // Make selected view and position it
674         mFirstPosition = mSelectedPosition;
675 
676         if (mAdapter != null) {
677             View sel = makeView(mSelectedPosition, true);
678             int width = sel.getMeasuredWidth();
679             int selectedOffset = childrenLeft;
680             final int layoutDirection = getLayoutDirection();
681             final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
682             switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
683                 case Gravity.CENTER_HORIZONTAL:
684                     selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
685                     break;
686                 case Gravity.RIGHT:
687                     selectedOffset = childrenLeft + childrenWidth - width;
688                     break;
689             }
690             sel.offsetLeftAndRight(selectedOffset);
691         }
692 
693         // Flush any cached views that did not get reused above
694         mRecycler.clear();
695 
696         invalidate();
697 
698         checkSelectionChanged();
699 
700         mDataChanged = false;
701         mNeedSync = false;
702         setNextSelectedPositionInt(mSelectedPosition);
703     }
704 
705     /**
706      * Obtain a view, either by pulling an existing view from the recycler or
707      * by getting a new one from the adapter. If we are animating, make sure
708      * there is enough information in the view's layout parameters to animate
709      * from the old to new positions.
710      *
711      * @param position Position in the spinner for the view to obtain
712      * @param addChild true to add the child to the spinner, false to obtain and configure only.
713      * @return A view for the given position
714      */
makeView(int position, boolean addChild)715     private View makeView(int position, boolean addChild) {
716         View child;
717 
718         if (!mDataChanged) {
719             child = mRecycler.get(position);
720             if (child != null) {
721                 // Position the view
722                 setUpChild(child, addChild);
723 
724                 return child;
725             }
726         }
727 
728         // Nothing found in the recycler -- ask the adapter for a view
729         child = mAdapter.getView(position, null, this);
730 
731         // Position the view
732         setUpChild(child, addChild);
733 
734         return child;
735     }
736 
737     /**
738      * Helper for makeAndAddView to set the position of a view
739      * and fill out its layout paramters.
740      *
741      * @param child The view to position
742      * @param addChild true if the child should be added to the Spinner during setup
743      */
setUpChild(View child, boolean addChild)744     private void setUpChild(View child, boolean addChild) {
745 
746         // Respect layout params that are already in the view. Otherwise
747         // make some up...
748         ViewGroup.LayoutParams lp = child.getLayoutParams();
749         if (lp == null) {
750             lp = generateDefaultLayoutParams();
751         }
752 
753         addViewInLayout(child, 0, lp);
754 
755         child.setSelected(hasFocus());
756         if (mDisableChildrenWhenDisabled) {
757             child.setEnabled(isEnabled());
758         }
759 
760         // Get measure specs
761         int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
762                 mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
763         int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
764                 mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
765 
766         // Measure child
767         child.measure(childWidthSpec, childHeightSpec);
768 
769         int childLeft;
770         int childRight;
771 
772         // Position vertically based on gravity setting
773         int childTop = mSpinnerPadding.top
774                 + ((getMeasuredHeight() - mSpinnerPadding.bottom -
775                         mSpinnerPadding.top - child.getMeasuredHeight()) / 2);
776         int childBottom = childTop + child.getMeasuredHeight();
777 
778         int width = child.getMeasuredWidth();
779         childLeft = 0;
780         childRight = childLeft + width;
781 
782         child.layout(childLeft, childTop, childRight, childBottom);
783 
784         if (!addChild) {
785             removeViewInLayout(child);
786         }
787     }
788 
789     @Override
performClick()790     public boolean performClick() {
791         boolean handled = super.performClick();
792 
793         if (!handled) {
794             handled = true;
795 
796             if (!mPopup.isShowing()) {
797                 mPopup.show(getTextDirection(), getTextAlignment());
798             }
799         }
800 
801         return handled;
802     }
803 
804     @Override
onClick(DialogInterface dialog, int which)805     public void onClick(DialogInterface dialog, int which) {
806         setSelection(which);
807         dialog.dismiss();
808     }
809 
810     /**
811      * Sets selection and dismisses the spinner's popup if it can be dismissed.
812      * For ease of use in tests, where publicly obtaining the spinner's popup is difficult.
813      *
814      * @param which index of the item to be selected.
815      * @hide
816      */
817     @TestApi
onClick(int which)818     public void onClick(int which) {
819         setSelection(which);
820         if (mPopup != null && mPopup.isShowing()) {
821             mPopup.dismiss();
822         }
823     }
824 
825     @Override
getAccessibilityClassName()826     public CharSequence getAccessibilityClassName() {
827         return Spinner.class.getName();
828     }
829 
830     /** @hide */
831     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)832     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
833         super.onInitializeAccessibilityNodeInfoInternal(info);
834 
835         if (mAdapter != null) {
836             info.setCanOpenPopup(true);
837         }
838     }
839 
840     /**
841      * Sets the prompt to display when the dialog is shown.
842      * @param prompt the prompt to set
843      */
setPrompt(CharSequence prompt)844     public void setPrompt(CharSequence prompt) {
845         mPopup.setPromptText(prompt);
846     }
847 
848     /**
849      * Sets the prompt to display when the dialog is shown.
850      * @param promptId the resource ID of the prompt to display when the dialog is shown
851      */
setPromptId(int promptId)852     public void setPromptId(int promptId) {
853         setPrompt(getContext().getText(promptId));
854     }
855 
856     /**
857      * @return The prompt to display when the dialog is shown
858      */
859     @InspectableProperty
getPrompt()860     public CharSequence getPrompt() {
861         return mPopup.getHintText();
862     }
863 
measureContentWidth(SpinnerAdapter adapter, Drawable background)864     int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
865         if (adapter == null) {
866             return 0;
867         }
868 
869         int width = 0;
870         View itemView = null;
871         int itemType = 0;
872         final int widthMeasureSpec =
873             MeasureSpec.makeSafeMeasureSpec(getMeasuredWidth(), MeasureSpec.UNSPECIFIED);
874         final int heightMeasureSpec =
875             MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED);
876 
877         // Make sure the number of items we'll measure is capped. If it's a huge data set
878         // with wildly varying sizes, oh well.
879         int start = Math.max(0, getSelectedItemPosition());
880         final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
881         final int count = end - start;
882         start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
883         for (int i = start; i < end; i++) {
884             final int positionType = adapter.getItemViewType(i);
885             if (positionType != itemType) {
886                 itemType = positionType;
887                 itemView = null;
888             }
889             itemView = adapter.getView(i, itemView, this);
890             if (itemView.getLayoutParams() == null) {
891                 itemView.setLayoutParams(new ViewGroup.LayoutParams(
892                         ViewGroup.LayoutParams.WRAP_CONTENT,
893                         ViewGroup.LayoutParams.WRAP_CONTENT));
894             }
895             itemView.measure(widthMeasureSpec, heightMeasureSpec);
896             width = Math.max(width, itemView.getMeasuredWidth());
897         }
898 
899         // Add background padding to measured width
900         if (background != null) {
901             background.getPadding(mTempRect);
902             width += mTempRect.left + mTempRect.right;
903         }
904 
905         return width;
906     }
907 
908     @Override
onSaveInstanceState()909     public Parcelable onSaveInstanceState() {
910         final SavedState ss = new SavedState(super.onSaveInstanceState());
911         ss.showDropdown = mPopup != null && mPopup.isShowing();
912         return ss;
913     }
914 
915     @Override
onRestoreInstanceState(Parcelable state)916     public void onRestoreInstanceState(Parcelable state) {
917         SavedState ss = (SavedState) state;
918 
919         super.onRestoreInstanceState(ss.getSuperState());
920 
921         if (ss.showDropdown) {
922             ViewTreeObserver vto = getViewTreeObserver();
923             if (vto != null) {
924                 final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
925                     @Override
926                     public void onGlobalLayout() {
927                         if (!mPopup.isShowing()) {
928                             mPopup.show(getTextDirection(), getTextAlignment());
929                         }
930                         final ViewTreeObserver vto = getViewTreeObserver();
931                         if (vto != null) {
932                             vto.removeOnGlobalLayoutListener(this);
933                         }
934                     }
935                 };
936                 vto.addOnGlobalLayoutListener(listener);
937             }
938         }
939     }
940 
941     @FlaggedApi(FLAG_ENABLE_ARROW_ICON_ON_HOVER_WHEN_CLICKABLE)
942     @Override
onResolvePointerIcon(MotionEvent event, int pointerIndex)943     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
944         if (getPointerIcon() == null && isClickable() && isEnabled()
945                 && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
946             int pointerIcon = enableArrowIconOnHoverWhenClickable()
947                     ? PointerIcon.TYPE_ARROW
948                     : PointerIcon.TYPE_HAND;
949             return PointerIcon.getSystemIcon(getContext(), pointerIcon);
950         }
951         return super.onResolvePointerIcon(event, pointerIndex);
952     }
953 
954     static class SavedState extends AbsSpinner.SavedState {
955         boolean showDropdown;
956 
SavedState(Parcelable superState)957         SavedState(Parcelable superState) {
958             super(superState);
959         }
960 
SavedState(Parcel in)961         private SavedState(Parcel in) {
962             super(in);
963             showDropdown = in.readByte() != 0;
964         }
965 
966         @Override
writeToParcel(Parcel out, int flags)967         public void writeToParcel(Parcel out, int flags) {
968             super.writeToParcel(out, flags);
969             out.writeByte((byte) (showDropdown ? 1 : 0));
970         }
971 
972         public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR =
973                 new Parcelable.Creator<SavedState>() {
974             public SavedState createFromParcel(Parcel in) {
975                 return new SavedState(in);
976             }
977 
978             public SavedState[] newArray(int size) {
979                 return new SavedState[size];
980             }
981         };
982     }
983 
984     /**
985      * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
986      * into a ListAdapter.</p>
987      */
988     private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
989         private SpinnerAdapter mAdapter;
990         private ListAdapter mListAdapter;
991 
992         /**
993          * Creates a new ListAdapter wrapper for the specified adapter.
994          *
995          * @param adapter the SpinnerAdapter to transform into a ListAdapter
996          * @param dropDownTheme the theme against which to inflate drop-down
997          *                      views, may be {@null} to use default theme
998          */
DropDownAdapter(@ullable SpinnerAdapter adapter, @Nullable Resources.Theme dropDownTheme)999         public DropDownAdapter(@Nullable SpinnerAdapter adapter,
1000                 @Nullable Resources.Theme dropDownTheme) {
1001             mAdapter = adapter;
1002 
1003             if (adapter instanceof ListAdapter) {
1004                 mListAdapter = (ListAdapter) adapter;
1005             }
1006 
1007             if (dropDownTheme != null && adapter instanceof ThemedSpinnerAdapter) {
1008                 final ThemedSpinnerAdapter themedAdapter = (ThemedSpinnerAdapter) adapter;
1009                 if (themedAdapter.getDropDownViewTheme() == null) {
1010                     themedAdapter.setDropDownViewTheme(dropDownTheme);
1011                 }
1012             }
1013         }
1014 
getCount()1015         public int getCount() {
1016             return mAdapter == null ? 0 : mAdapter.getCount();
1017         }
1018 
getItem(int position)1019         public Object getItem(int position) {
1020             return mAdapter == null ? null : mAdapter.getItem(position);
1021         }
1022 
getItemId(int position)1023         public long getItemId(int position) {
1024             return mAdapter == null ? -1 : mAdapter.getItemId(position);
1025         }
1026 
getView(int position, View convertView, ViewGroup parent)1027         public View getView(int position, View convertView, ViewGroup parent) {
1028             return getDropDownView(position, convertView, parent);
1029         }
1030 
getDropDownView(int position, View convertView, ViewGroup parent)1031         public View getDropDownView(int position, View convertView, ViewGroup parent) {
1032             return (mAdapter == null) ? null : mAdapter.getDropDownView(position, convertView, parent);
1033         }
1034 
hasStableIds()1035         public boolean hasStableIds() {
1036             return mAdapter != null && mAdapter.hasStableIds();
1037         }
1038 
registerDataSetObserver(DataSetObserver observer)1039         public void registerDataSetObserver(DataSetObserver observer) {
1040             if (mAdapter != null) {
1041                 mAdapter.registerDataSetObserver(observer);
1042             }
1043         }
1044 
unregisterDataSetObserver(DataSetObserver observer)1045         public void unregisterDataSetObserver(DataSetObserver observer) {
1046             if (mAdapter != null) {
1047                 mAdapter.unregisterDataSetObserver(observer);
1048             }
1049         }
1050 
1051         /**
1052          * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
1053          * Otherwise, return true.
1054          */
areAllItemsEnabled()1055         public boolean areAllItemsEnabled() {
1056             final ListAdapter adapter = mListAdapter;
1057             if (adapter != null) {
1058                 return adapter.areAllItemsEnabled();
1059             } else {
1060                 return true;
1061             }
1062         }
1063 
1064         /**
1065          * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
1066          * Otherwise, return true.
1067          */
isEnabled(int position)1068         public boolean isEnabled(int position) {
1069             final ListAdapter adapter = mListAdapter;
1070             if (adapter != null) {
1071                 return adapter.isEnabled(position);
1072             } else {
1073                 return true;
1074             }
1075         }
1076 
getItemViewType(int position)1077         public int getItemViewType(int position) {
1078             return 0;
1079         }
1080 
getViewTypeCount()1081         public int getViewTypeCount() {
1082             return 1;
1083         }
1084 
isEmpty()1085         public boolean isEmpty() {
1086             return getCount() == 0;
1087         }
1088     }
1089 
1090     /**
1091      * Implements some sort of popup selection interface for selecting a spinner option.
1092      * Allows for different spinner modes.
1093      */
1094     private interface SpinnerPopup {
setAdapter(ListAdapter adapter)1095         public void setAdapter(ListAdapter adapter);
1096 
1097         /**
1098          * Show the popup
1099          */
show(int textDirection, int textAlignment)1100         public void show(int textDirection, int textAlignment);
1101 
1102         /**
1103          * Dismiss the popup
1104          */
dismiss()1105         public void dismiss();
1106 
1107         /**
1108          * @return true if the popup is showing, false otherwise.
1109          */
1110         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
isShowing()1111         public boolean isShowing();
1112 
1113         /**
1114          * Set hint text to be displayed to the user. This should provide
1115          * a description of the choice being made.
1116          * @param hintText Hint text to set.
1117          */
setPromptText(CharSequence hintText)1118         public void setPromptText(CharSequence hintText);
getHintText()1119         public CharSequence getHintText();
1120 
setBackgroundDrawable(Drawable bg)1121         public void setBackgroundDrawable(Drawable bg);
setVerticalOffset(int px)1122         public void setVerticalOffset(int px);
setHorizontalOffset(int px)1123         public void setHorizontalOffset(int px);
getBackground()1124         public Drawable getBackground();
getVerticalOffset()1125         public int getVerticalOffset();
getHorizontalOffset()1126         public int getHorizontalOffset();
1127     }
1128 
1129     private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
1130         private AlertDialog mPopup;
1131         private ListAdapter mListAdapter;
1132         private CharSequence mPrompt;
1133 
dismiss()1134         public void dismiss() {
1135             if (mPopup != null) {
1136                 mPopup.dismiss();
1137                 mPopup = null;
1138             }
1139         }
1140 
1141         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
isShowing()1142         public boolean isShowing() {
1143             return mPopup != null ? mPopup.isShowing() : false;
1144         }
1145 
setAdapter(ListAdapter adapter)1146         public void setAdapter(ListAdapter adapter) {
1147             mListAdapter = adapter;
1148         }
1149 
setPromptText(CharSequence hintText)1150         public void setPromptText(CharSequence hintText) {
1151             mPrompt = hintText;
1152         }
1153 
getHintText()1154         public CharSequence getHintText() {
1155             return mPrompt;
1156         }
1157 
show(int textDirection, int textAlignment)1158         public void show(int textDirection, int textAlignment) {
1159             if (mListAdapter == null) {
1160                 return;
1161             }
1162             AlertDialog.Builder builder = new AlertDialog.Builder(getPopupContext());
1163             if (mPrompt != null) {
1164                 builder.setTitle(mPrompt);
1165             }
1166             mPopup = builder.setSingleChoiceItems(mListAdapter,
1167                     getSelectedItemPosition(), this).create();
1168             final ListView listView = mPopup.getListView();
1169             listView.setTextDirection(textDirection);
1170             listView.setTextAlignment(textAlignment);
1171             mPopup.show();
1172         }
1173 
onClick(DialogInterface dialog, int which)1174         public void onClick(DialogInterface dialog, int which) {
1175             setSelection(which);
1176             if (mOnItemClickListener != null) {
1177                 performItemClick(null, which, mListAdapter.getItemId(which));
1178             }
1179             dismiss();
1180         }
1181 
1182         @Override
setBackgroundDrawable(Drawable bg)1183         public void setBackgroundDrawable(Drawable bg) {
1184             Log.e(TAG, "Cannot set popup background for MODE_DIALOG, ignoring");
1185         }
1186 
1187         @Override
setVerticalOffset(int px)1188         public void setVerticalOffset(int px) {
1189             Log.e(TAG, "Cannot set vertical offset for MODE_DIALOG, ignoring");
1190         }
1191 
1192         @Override
setHorizontalOffset(int px)1193         public void setHorizontalOffset(int px) {
1194             Log.e(TAG, "Cannot set horizontal offset for MODE_DIALOG, ignoring");
1195         }
1196 
1197         @Override
getBackground()1198         public Drawable getBackground() {
1199             return null;
1200         }
1201 
1202         @Override
getVerticalOffset()1203         public int getVerticalOffset() {
1204             return 0;
1205         }
1206 
1207         @Override
getHorizontalOffset()1208         public int getHorizontalOffset() {
1209             return 0;
1210         }
1211     }
1212 
1213     private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
1214         private CharSequence mHintText;
1215         private ListAdapter mAdapter;
1216 
DropdownPopup( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)1217         public DropdownPopup(
1218                 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
1219             super(context, attrs, defStyleAttr, defStyleRes);
1220 
1221             setAnchorView(Spinner.this);
1222             setModal(true);
1223             setPromptPosition(POSITION_PROMPT_ABOVE);
1224             setOnItemClickListener(new OnItemClickListener() {
1225                 public void onItemClick(AdapterView parent, View v, int position, long id) {
1226                     Spinner.this.setSelection(position);
1227                     if (mOnItemClickListener != null) {
1228                         Spinner.this.performItemClick(v, position, mAdapter.getItemId(position));
1229                     }
1230                     dismiss();
1231                 }
1232             });
1233         }
1234 
1235         @Override
setAdapter(ListAdapter adapter)1236         public void setAdapter(ListAdapter adapter) {
1237             super.setAdapter(adapter);
1238             mAdapter = adapter;
1239         }
1240 
getHintText()1241         public CharSequence getHintText() {
1242             return mHintText;
1243         }
1244 
setPromptText(CharSequence hintText)1245         public void setPromptText(CharSequence hintText) {
1246             // Hint text is ignored for dropdowns, but maintain it here.
1247             mHintText = hintText;
1248         }
1249 
computeContentWidth()1250         void computeContentWidth() {
1251             final Drawable background = getBackground();
1252             int hOffset = 0;
1253             if (background != null) {
1254                 background.getPadding(mTempRect);
1255                 hOffset = isLayoutRtl() ? mTempRect.right : -mTempRect.left;
1256             } else {
1257                 mTempRect.left = mTempRect.right = 0;
1258             }
1259 
1260             final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
1261             final int spinnerPaddingRight = Spinner.this.getPaddingRight();
1262             final int spinnerWidth = Spinner.this.getWidth();
1263 
1264             if (mDropDownWidth == WRAP_CONTENT) {
1265                 int contentWidth =  measureContentWidth(
1266                         (SpinnerAdapter) mAdapter, getBackground());
1267                 final int contentWidthLimit = mContext.getResources()
1268                         .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
1269                 if (contentWidth > contentWidthLimit) {
1270                     contentWidth = contentWidthLimit;
1271                 }
1272                 setContentWidth(Math.max(
1273                        contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
1274             } else if (mDropDownWidth == MATCH_PARENT) {
1275                 setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
1276             } else {
1277                 setContentWidth(mDropDownWidth);
1278             }
1279 
1280             if (isLayoutRtl()) {
1281                 hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
1282             } else {
1283                 hOffset += spinnerPaddingLeft;
1284             }
1285             setHorizontalOffset(hOffset);
1286         }
1287 
show(int textDirection, int textAlignment)1288         public void show(int textDirection, int textAlignment) {
1289             final boolean wasShowing = isShowing();
1290 
1291             computeContentWidth();
1292 
1293             setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1294             super.show();
1295             final ListView listView = getListView();
1296             listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
1297             listView.setTextDirection(textDirection);
1298             listView.setTextAlignment(textAlignment);
1299             setSelection(Spinner.this.getSelectedItemPosition());
1300 
1301             if (wasShowing) {
1302                 // Skip setting up the layout/dismiss listener below. If we were previously
1303                 // showing it will still stick around.
1304                 return;
1305             }
1306 
1307             // Make sure we hide if our anchor goes away.
1308             // TODO: This might be appropriate to push all the way down to PopupWindow,
1309             // but it may have other side effects to investigate first. (Text editing handles, etc.)
1310             final ViewTreeObserver vto = getViewTreeObserver();
1311             if (vto != null) {
1312                 final OnGlobalLayoutListener layoutListener = new OnGlobalLayoutListener() {
1313                     @Override
1314                     public void onGlobalLayout() {
1315                         if (!Spinner.this.isVisibleToUser()) {
1316                             dismiss();
1317                         } else {
1318                             computeContentWidth();
1319 
1320                             // Use super.show here to update; we don't want to move the selected
1321                             // position or adjust other things that would be reset otherwise.
1322                             DropdownPopup.super.show();
1323                         }
1324                     }
1325                 };
1326                 vto.addOnGlobalLayoutListener(layoutListener);
1327                 setOnDismissListener(new OnDismissListener() {
1328                     @Override public void onDismiss() {
1329                         final ViewTreeObserver vto = getViewTreeObserver();
1330                         if (vto != null) {
1331                             vto.removeOnGlobalLayoutListener(layoutListener);
1332                         }
1333                     }
1334                 });
1335             }
1336         }
1337     }
1338 
1339 }
1340