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