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