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