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