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