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