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