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