Spinner.java revision 42b7e99b11a5ab1cbc0beebe0b15e46bdf462dff
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.view.Gravity;
30import android.view.View;
31import android.view.ViewGroup;
32
33
34/**
35 * A view that displays one child at a time and lets the user pick among them.
36 * The items in the Spinner come from the {@link Adapter} associated with
37 * this view.
38 *
39 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-spinner.html">Spinner
40 * tutorial</a>.</p>
41 *
42 * @attr ref android.R.styleable#Spinner_prompt
43 */
44@Widget
45public class Spinner extends AbsSpinner implements OnClickListener {
46    private static final String TAG = "Spinner";
47
48    // Only measure this many items to get a decent max width.
49    private static final int MAX_ITEMS_MEASURED = 15;
50
51    /**
52     * Use a dialog window for selecting spinner options.
53     */
54    public static final int MODE_DIALOG = 0;
55
56    /**
57     * Use a dropdown anchored to the Spinner for selecting spinner options.
58     */
59    public static final int MODE_DROPDOWN = 1;
60
61    /**
62     * Use the theme-supplied value to select the dropdown mode.
63     */
64    private static final int MODE_THEME = -1;
65
66    private SpinnerPopup mPopup;
67    private DropDownAdapter mTempAdapter;
68    int mDropDownWidth;
69
70    private int mGravity;
71    private boolean mDisableChildrenWhenDisabled;
72
73    private Rect mTempRect = new Rect();
74
75    /**
76     * Construct a new spinner with the given context's theme.
77     *
78     * @param context The Context the view is running in, through which it can
79     *        access the current theme, resources, etc.
80     */
81    public Spinner(Context context) {
82        this(context, null);
83    }
84
85    /**
86     * Construct a new spinner with the given context's theme and the supplied
87     * mode of displaying choices. <code>mode</code> may be one of
88     * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN}.
89     *
90     * @param context The Context the view is running in, through which it can
91     *        access the current theme, resources, etc.
92     * @param mode Constant describing how the user will select choices from the spinner.
93     *
94     * @see #MODE_DIALOG
95     * @see #MODE_DROPDOWN
96     */
97    public Spinner(Context context, int mode) {
98        this(context, null, com.android.internal.R.attr.spinnerStyle, mode);
99    }
100
101    /**
102     * Construct a new spinner with the given context's theme and the supplied attribute set.
103     *
104     * @param context The Context the view is running in, through which it can
105     *        access the current theme, resources, etc.
106     * @param attrs The attributes of the XML tag that is inflating the view.
107     */
108    public Spinner(Context context, AttributeSet attrs) {
109        this(context, attrs, com.android.internal.R.attr.spinnerStyle);
110    }
111
112    /**
113     * Construct a new spinner with the given context's theme, the supplied attribute set,
114     * and default style.
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     * @param defStyle The default style to apply to this view. If 0, no style
120     *        will be applied (beyond what is included in the theme). This may
121     *        either be an attribute resource, whose value will be retrieved
122     *        from the current theme, or an explicit style resource.
123     */
124    public Spinner(Context context, AttributeSet attrs, int defStyle) {
125        this(context, attrs, defStyle, MODE_THEME);
126    }
127
128    /**
129     * Construct a new spinner with the given context's theme, the supplied attribute set,
130     * and default style. <code>mode</code> may be one of {@link #MODE_DIALOG} or
131     * {@link #MODE_DROPDOWN} and determines how the user will select choices from the spinner.
132     *
133     * @param context The Context the view is running in, through which it can
134     *        access the current theme, resources, etc.
135     * @param attrs The attributes of the XML tag that is inflating the view.
136     * @param defStyle The default style to apply to this view. If 0, no style
137     *        will be applied (beyond what is included in the theme). This may
138     *        either be an attribute resource, whose value will be retrieved
139     *        from the current theme, or an explicit style resource.
140     * @param mode Constant describing how the user will select choices from the spinner.
141     *
142     * @see #MODE_DIALOG
143     * @see #MODE_DROPDOWN
144     */
145    public Spinner(Context context, AttributeSet attrs, int defStyle, int mode) {
146        super(context, attrs, defStyle);
147
148        TypedArray a = context.obtainStyledAttributes(attrs,
149                com.android.internal.R.styleable.Spinner, defStyle, 0);
150
151        if (mode == MODE_THEME) {
152            mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode, MODE_DIALOG);
153        }
154
155        switch (mode) {
156        case MODE_DIALOG: {
157            mPopup = new DialogPopup();
158            break;
159        }
160
161        case MODE_DROPDOWN: {
162            DropdownPopup popup = new DropdownPopup(context, attrs, defStyle);
163
164            mDropDownWidth = a.getLayoutDimension(
165                    com.android.internal.R.styleable.Spinner_dropDownWidth,
166                    ViewGroup.LayoutParams.WRAP_CONTENT);
167            popup.setBackgroundDrawable(a.getDrawable(
168                    com.android.internal.R.styleable.Spinner_popupBackground));
169            final int verticalOffset = a.getDimensionPixelOffset(
170                    com.android.internal.R.styleable.Spinner_dropDownVerticalOffset, 0);
171            if (verticalOffset != 0) {
172                popup.setVerticalOffset(verticalOffset);
173            }
174
175            final int horizontalOffset = a.getDimensionPixelOffset(
176                    com.android.internal.R.styleable.Spinner_dropDownHorizontalOffset, 0);
177            if (horizontalOffset != 0) {
178                popup.setHorizontalOffset(horizontalOffset);
179            }
180
181            mPopup = popup;
182            break;
183        }
184        }
185
186        mGravity = a.getInt(com.android.internal.R.styleable.Spinner_gravity, Gravity.CENTER);
187
188        mPopup.setPromptText(a.getString(com.android.internal.R.styleable.Spinner_prompt));
189
190        mDisableChildrenWhenDisabled = a.getBoolean(
191                com.android.internal.R.styleable.Spinner_disableChildrenWhenDisabled, false);
192
193        a.recycle();
194
195        // Base constructor can call setAdapter before we initialize mPopup.
196        // Finish setting things up if this happened.
197        if (mTempAdapter != null) {
198            mPopup.setAdapter(mTempAdapter);
199            mTempAdapter = null;
200        }
201    }
202
203    @Override
204    public void setEnabled(boolean enabled) {
205        super.setEnabled(enabled);
206        if (mDisableChildrenWhenDisabled) {
207            final int count = getChildCount();
208            for (int i = 0; i < count; i++) {
209                getChildAt(i).setEnabled(enabled);
210            }
211        }
212    }
213
214    /**
215     * Describes how the selected item view is positioned. Currently only the horizontal component
216     * is used. The default is determined by the current theme.
217     *
218     * @param gravity See {@link android.view.Gravity}
219     *
220     * @attr ref android.R.styleable#Spinner_gravity
221     */
222    public void setGravity(int gravity) {
223        if (mGravity != gravity) {
224            if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
225                gravity |= Gravity.LEFT;
226            }
227            mGravity = gravity;
228            requestLayout();
229        }
230    }
231
232    @Override
233    public void setAdapter(SpinnerAdapter adapter) {
234        super.setAdapter(adapter);
235
236        if (mPopup != null) {
237            mPopup.setAdapter(new DropDownAdapter(adapter));
238        } else {
239            mTempAdapter = new DropDownAdapter(adapter);
240        }
241    }
242
243    @Override
244    public int getBaseline() {
245        View child = null;
246
247        if (getChildCount() > 0) {
248            child = getChildAt(0);
249        } else if (mAdapter != null && mAdapter.getCount() > 0) {
250            child = makeAndAddView(0);
251            mRecycler.put(0, child);
252            removeAllViewsInLayout();
253        }
254
255        if (child != null) {
256            final int childBaseline = child.getBaseline();
257            return childBaseline >= 0 ? child.getTop() + childBaseline : -1;
258        } else {
259            return -1;
260        }
261    }
262
263    @Override
264    protected void onDetachedFromWindow() {
265        super.onDetachedFromWindow();
266
267        if (mPopup != null && mPopup.isShowing()) {
268            mPopup.dismiss();
269        }
270    }
271
272    /**
273     * <p>A spinner does not support item click events. Calling this method
274     * will raise an exception.</p>
275     *
276     * @param l this listener will be ignored
277     */
278    @Override
279    public void setOnItemClickListener(OnItemClickListener l) {
280        throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
281    }
282
283    @Override
284    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
285        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
286        if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
287            final int measuredWidth = getMeasuredWidth();
288            setMeasuredDimension(Math.min(Math.max(measuredWidth,
289                    measureContentWidth(getAdapter(), getBackground())),
290                    MeasureSpec.getSize(widthMeasureSpec)),
291                    getMeasuredHeight());
292        }
293    }
294
295    /**
296     * @see android.view.View#onLayout(boolean,int,int,int,int)
297     *
298     * Creates and positions all views
299     *
300     */
301    @Override
302    protected void onLayout(boolean changed, int l, int t, int r, int b) {
303        super.onLayout(changed, l, t, r, b);
304        mInLayout = true;
305        layout(0, false);
306        mInLayout = false;
307    }
308
309    /**
310     * Creates and positions all views for this Spinner.
311     *
312     * @param delta Change in the selected position. +1 moves selection is moving to the right,
313     * so views are scrolling to the left. -1 means selection is moving to the left.
314     */
315    @Override
316    void layout(int delta, boolean animate) {
317        int childrenLeft = mSpinnerPadding.left;
318        int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
319
320        if (mDataChanged) {
321            handleDataChanged();
322        }
323
324        // Handle the empty set by removing all views
325        if (mItemCount == 0) {
326            resetList();
327            return;
328        }
329
330        if (mNextSelectedPosition >= 0) {
331            setSelectedPositionInt(mNextSelectedPosition);
332        }
333
334        recycleAllViews();
335
336        // Clear out old views
337        removeAllViewsInLayout();
338
339        // Make selected view and position it
340        mFirstPosition = mSelectedPosition;
341        View sel = makeAndAddView(mSelectedPosition);
342        int width = sel.getMeasuredWidth();
343        int selectedOffset = childrenLeft;
344        switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
345            case Gravity.CENTER_HORIZONTAL:
346                selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
347                break;
348            case Gravity.RIGHT:
349                selectedOffset = childrenLeft + childrenWidth - width;
350                break;
351        }
352        sel.offsetLeftAndRight(selectedOffset);
353
354        // Flush any cached views that did not get reused above
355        mRecycler.clear();
356
357        invalidate();
358
359        checkSelectionChanged();
360
361        mDataChanged = false;
362        mNeedSync = false;
363        setNextSelectedPositionInt(mSelectedPosition);
364    }
365
366    /**
367     * Obtain a view, either by pulling an existing view from the recycler or
368     * by getting a new one from the adapter. If we are animating, make sure
369     * there is enough information in the view's layout parameters to animate
370     * from the old to new positions.
371     *
372     * @param position Position in the spinner for the view to obtain
373     * @return A view that has been added to the spinner
374     */
375    private View makeAndAddView(int position) {
376
377        View child;
378
379        if (!mDataChanged) {
380            child = mRecycler.get(position);
381            if (child != null) {
382                // Position the view
383                setUpChild(child);
384
385                return child;
386            }
387        }
388
389        // Nothing found in the recycler -- ask the adapter for a view
390        child = mAdapter.getView(position, null, this);
391
392        // Position the view
393        setUpChild(child);
394
395        return child;
396    }
397
398    /**
399     * Helper for makeAndAddView to set the position of a view
400     * and fill out its layout paramters.
401     *
402     * @param child The view to position
403     */
404    private void setUpChild(View child) {
405
406        // Respect layout params that are already in the view. Otherwise
407        // make some up...
408        ViewGroup.LayoutParams lp = child.getLayoutParams();
409        if (lp == null) {
410            lp = generateDefaultLayoutParams();
411        }
412
413        addViewInLayout(child, 0, lp);
414
415        child.setSelected(hasFocus());
416        if (mDisableChildrenWhenDisabled) {
417            child.setEnabled(isEnabled());
418        }
419
420        // Get measure specs
421        int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
422                mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
423        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
424                mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
425
426        // Measure child
427        child.measure(childWidthSpec, childHeightSpec);
428
429        int childLeft;
430        int childRight;
431
432        // Position vertically based on gravity setting
433        int childTop = mSpinnerPadding.top
434                + ((getMeasuredHeight() - mSpinnerPadding.bottom -
435                        mSpinnerPadding.top - child.getMeasuredHeight()) / 2);
436        int childBottom = childTop + child.getMeasuredHeight();
437
438        int width = child.getMeasuredWidth();
439        childLeft = 0;
440        childRight = childLeft + width;
441
442        child.layout(childLeft, childTop, childRight, childBottom);
443    }
444
445    @Override
446    public boolean performClick() {
447        boolean handled = super.performClick();
448
449        if (!handled) {
450            handled = true;
451
452            if (!mPopup.isShowing()) {
453                mPopup.show();
454            }
455        }
456
457        return handled;
458    }
459
460    public void onClick(DialogInterface dialog, int which) {
461        setSelection(which);
462        dialog.dismiss();
463    }
464
465    /**
466     * Sets the prompt to display when the dialog is shown.
467     * @param prompt the prompt to set
468     */
469    public void setPrompt(CharSequence prompt) {
470        mPopup.setPromptText(prompt);
471    }
472
473    /**
474     * Sets the prompt to display when the dialog is shown.
475     * @param promptId the resource ID of the prompt to display when the dialog is shown
476     */
477    public void setPromptId(int promptId) {
478        setPrompt(getContext().getText(promptId));
479    }
480
481    /**
482     * @return The prompt to display when the dialog is shown
483     */
484    public CharSequence getPrompt() {
485        return mPopup.getHintText();
486    }
487
488    int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
489        if (adapter == null) {
490            return 0;
491        }
492
493        int width = 0;
494        View itemView = null;
495        int itemType = 0;
496        final int widthMeasureSpec =
497            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
498        final int heightMeasureSpec =
499            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
500
501        // Make sure the number of items we'll measure is capped. If it's a huge data set
502        // with wildly varying sizes, oh well.
503        int start = Math.max(0, getSelectedItemPosition());
504        final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
505        final int count = end - start;
506        start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
507        for (int i = start; i < end; i++) {
508            final int positionType = adapter.getItemViewType(i);
509            if (positionType != itemType) {
510                itemType = positionType;
511                itemView = null;
512            }
513            itemView = adapter.getView(i, itemView, this);
514            if (itemView.getLayoutParams() == null) {
515                itemView.setLayoutParams(new ViewGroup.LayoutParams(
516                        ViewGroup.LayoutParams.WRAP_CONTENT,
517                        ViewGroup.LayoutParams.WRAP_CONTENT));
518            }
519            itemView.measure(widthMeasureSpec, heightMeasureSpec);
520            width = Math.max(width, itemView.getMeasuredWidth());
521        }
522
523        // Add background padding to measured width
524        if (background != null) {
525            background.getPadding(mTempRect);
526            width += mTempRect.left + mTempRect.right;
527        }
528
529        return width;
530    }
531
532    /**
533     * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
534     * into a ListAdapter.</p>
535     */
536    private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
537        private SpinnerAdapter mAdapter;
538        private ListAdapter mListAdapter;
539
540        /**
541         * <p>Creates a new ListAdapter wrapper for the specified adapter.</p>
542         *
543         * @param adapter the Adapter to transform into a ListAdapter
544         */
545        public DropDownAdapter(SpinnerAdapter adapter) {
546            this.mAdapter = adapter;
547            if (adapter instanceof ListAdapter) {
548                this.mListAdapter = (ListAdapter) adapter;
549            }
550        }
551
552        public int getCount() {
553            return mAdapter == null ? 0 : mAdapter.getCount();
554        }
555
556        public Object getItem(int position) {
557            return mAdapter == null ? null : mAdapter.getItem(position);
558        }
559
560        public long getItemId(int position) {
561            return mAdapter == null ? -1 : mAdapter.getItemId(position);
562        }
563
564        public View getView(int position, View convertView, ViewGroup parent) {
565            return getDropDownView(position, convertView, parent);
566        }
567
568        public View getDropDownView(int position, View convertView, ViewGroup parent) {
569            return mAdapter == null ? null :
570                    mAdapter.getDropDownView(position, convertView, parent);
571        }
572
573        public boolean hasStableIds() {
574            return mAdapter != null && mAdapter.hasStableIds();
575        }
576
577        public void registerDataSetObserver(DataSetObserver observer) {
578            if (mAdapter != null) {
579                mAdapter.registerDataSetObserver(observer);
580            }
581        }
582
583        public void unregisterDataSetObserver(DataSetObserver observer) {
584            if (mAdapter != null) {
585                mAdapter.unregisterDataSetObserver(observer);
586            }
587        }
588
589        /**
590         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
591         * Otherwise, return true.
592         */
593        public boolean areAllItemsEnabled() {
594            final ListAdapter adapter = mListAdapter;
595            if (adapter != null) {
596                return adapter.areAllItemsEnabled();
597            } else {
598                return true;
599            }
600        }
601
602        /**
603         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
604         * Otherwise, return true.
605         */
606        public boolean isEnabled(int position) {
607            final ListAdapter adapter = mListAdapter;
608            if (adapter != null) {
609                return adapter.isEnabled(position);
610            } else {
611                return true;
612            }
613        }
614
615        public int getItemViewType(int position) {
616            return 0;
617        }
618
619        public int getViewTypeCount() {
620            return 1;
621        }
622
623        public boolean isEmpty() {
624            return getCount() == 0;
625        }
626    }
627
628    /**
629     * Implements some sort of popup selection interface for selecting a spinner option.
630     * Allows for different spinner modes.
631     */
632    private interface SpinnerPopup {
633        public void setAdapter(ListAdapter adapter);
634
635        /**
636         * Show the popup
637         */
638        public void show();
639
640        /**
641         * Dismiss the popup
642         */
643        public void dismiss();
644
645        /**
646         * @return true if the popup is showing, false otherwise.
647         */
648        public boolean isShowing();
649
650        /**
651         * Set hint text to be displayed to the user. This should provide
652         * a description of the choice being made.
653         * @param hintText Hint text to set.
654         */
655        public void setPromptText(CharSequence hintText);
656        public CharSequence getHintText();
657    }
658
659    private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
660        private AlertDialog mPopup;
661        private ListAdapter mListAdapter;
662        private CharSequence mPrompt;
663
664        public void dismiss() {
665            mPopup.dismiss();
666            mPopup = null;
667        }
668
669        public boolean isShowing() {
670            return mPopup != null ? mPopup.isShowing() : false;
671        }
672
673        public void setAdapter(ListAdapter adapter) {
674            mListAdapter = adapter;
675        }
676
677        public void setPromptText(CharSequence hintText) {
678            mPrompt = hintText;
679        }
680
681        public CharSequence getHintText() {
682            return mPrompt;
683        }
684
685        public void show() {
686            AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
687            if (mPrompt != null) {
688                builder.setTitle(mPrompt);
689            }
690            mPopup = builder.setSingleChoiceItems(mListAdapter,
691                    getSelectedItemPosition(), this).show();
692        }
693
694        public void onClick(DialogInterface dialog, int which) {
695            setSelection(which);
696            dismiss();
697        }
698    }
699
700    private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
701        private CharSequence mHintText;
702        private ListAdapter mAdapter;
703
704        public DropdownPopup(Context context, AttributeSet attrs, int defStyleRes) {
705            super(context, attrs, 0, defStyleRes);
706
707            setAnchorView(Spinner.this);
708            setModal(true);
709            setPromptPosition(POSITION_PROMPT_ABOVE);
710            setOnItemClickListener(new OnItemClickListener() {
711                public void onItemClick(AdapterView parent, View v, int position, long id) {
712                    Spinner.this.setSelection(position);
713                    dismiss();
714                }
715            });
716        }
717
718        @Override
719        public void setAdapter(ListAdapter adapter) {
720            super.setAdapter(adapter);
721            mAdapter = adapter;
722        }
723
724        public CharSequence getHintText() {
725            return mHintText;
726        }
727
728        public void setPromptText(CharSequence hintText) {
729            // Hint text is ignored for dropdowns, but maintain it here.
730            mHintText = hintText;
731        }
732
733        @Override
734        public void show() {
735            final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
736            if (mDropDownWidth == WRAP_CONTENT) {
737                final int spinnerWidth = Spinner.this.getWidth();
738                final int spinnerPaddingRight = Spinner.this.getPaddingRight();
739                setContentWidth(Math.max(
740                        measureContentWidth((SpinnerAdapter) mAdapter, getBackground()),
741                        spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
742            } else if (mDropDownWidth == MATCH_PARENT) {
743                final int spinnerWidth = Spinner.this.getWidth();
744                final int spinnerPaddingRight = Spinner.this.getPaddingRight();
745                setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
746            } else {
747                setContentWidth(mDropDownWidth);
748            }
749            final Drawable background = getBackground();
750            int bgOffset = 0;
751            if (background != null) {
752                background.getPadding(mTempRect);
753                bgOffset = -mTempRect.left;
754            }
755            setHorizontalOffset(bgOffset + spinnerPaddingLeft);
756            setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
757            super.show();
758            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
759            setSelection(Spinner.this.getSelectedItemPosition());
760        }
761    }
762}
763