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