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