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