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