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