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