Spinner.java revision 385a655b8e8bf85024e4f24f1d7f6c2d7d7e900d
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.util.AttributeSet;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.ViewGroup;
30
31
32/**
33 * A view that displays one child at a time and lets the user pick among them.
34 * The items in the Spinner come from the {@link Adapter} associated with
35 * this view.
36 *
37 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-spinner.html">Spinner
38 * tutorial</a>.</p>
39 *
40 * @attr ref android.R.styleable#Spinner_prompt
41 */
42@Widget
43public class Spinner extends AbsSpinner implements OnClickListener {
44    private static final String TAG = "Spinner";
45
46    /**
47     * Use a dialog window for selecting spinner options.
48     */
49    public static final int MODE_DIALOG = 0;
50
51    /**
52     * Use a dropdown anchored to the Spinner for selecting spinner options.
53     */
54    public static final int MODE_DROPDOWN = 1;
55
56    private SpinnerPopup mPopup;
57    private DropDownAdapter mTempAdapter;
58
59    public Spinner(Context context) {
60        this(context, null);
61    }
62
63    public Spinner(Context context, AttributeSet attrs) {
64        this(context, attrs, com.android.internal.R.attr.spinnerStyle);
65    }
66
67    public Spinner(Context context, AttributeSet attrs, int defStyle) {
68        super(context, attrs, defStyle);
69
70        TypedArray a = context.obtainStyledAttributes(attrs,
71                com.android.internal.R.styleable.Spinner, defStyle, 0);
72
73        final int mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode,
74                MODE_DIALOG);
75
76        switch (mode) {
77        case MODE_DIALOG: {
78            mPopup = new DialogPopup();
79            break;
80        }
81
82        case MODE_DROPDOWN: {
83            final int hintResource = a.getResourceId(
84                    com.android.internal.R.styleable.Spinner_popupPromptView, 0);
85
86            DropdownPopup popup = new DropdownPopup(context, attrs, defStyle, hintResource);
87
88            popup.setBackgroundDrawable(a.getDrawable(
89                    com.android.internal.R.styleable.Spinner_popupBackground));
90            popup.setVerticalOffset(a.getDimensionPixelOffset(
91                    com.android.internal.R.styleable.Spinner_dropDownVerticalOffset, 0));
92            popup.setHorizontalOffset(a.getDimensionPixelOffset(
93                    com.android.internal.R.styleable.Spinner_dropDownHorizontalOffset, 0));
94
95            mPopup = popup;
96            break;
97        }
98        }
99
100        mPopup.setPromptText(a.getString(com.android.internal.R.styleable.Spinner_prompt));
101
102        a.recycle();
103
104        // Base constructor can call setAdapter before we initialize mPopup.
105        // Finish setting things up if this happened.
106        if (mTempAdapter != null) {
107            mPopup.setAdapter(mTempAdapter);
108            mTempAdapter = null;
109        }
110    }
111
112    @Override
113    public void setAdapter(SpinnerAdapter adapter) {
114        super.setAdapter(adapter);
115
116        if (mPopup != null) {
117            mPopup.setAdapter(new DropDownAdapter(adapter));
118        } else {
119            mTempAdapter = new DropDownAdapter(adapter);
120        }
121    }
122
123    @Override
124    public int getBaseline() {
125        View child = null;
126
127        if (getChildCount() > 0) {
128            child = getChildAt(0);
129        } else if (mAdapter != null && mAdapter.getCount() > 0) {
130            child = makeAndAddView(0);
131            // TODO: We should probably put the child in the recycler
132        }
133
134        if (child != null) {
135            return child.getTop() + child.getBaseline();
136        } else {
137            return -1;
138        }
139    }
140
141    @Override
142    protected void onDetachedFromWindow() {
143        super.onDetachedFromWindow();
144
145        if (mPopup != null && mPopup.isShowing()) {
146            mPopup.dismiss();
147            mPopup = null;
148        }
149    }
150
151    /**
152     * <p>A spinner does not support item click events. Calling this method
153     * will raise an exception.</p>
154     *
155     * @param l this listener will be ignored
156     */
157    @Override
158    public void setOnItemClickListener(OnItemClickListener l) {
159        throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
160    }
161
162    /**
163     * @see android.view.View#onLayout(boolean,int,int,int,int)
164     *
165     * Creates and positions all views
166     *
167     */
168    @Override
169    protected void onLayout(boolean changed, int l, int t, int r, int b) {
170        super.onLayout(changed, l, t, r, b);
171        mInLayout = true;
172        layout(0, false);
173        mInLayout = false;
174    }
175
176    /**
177     * Creates and positions all views for this Spinner.
178     *
179     * @param delta Change in the selected position. +1 moves selection is moving to the right,
180     * so views are scrolling to the left. -1 means selection is moving to the left.
181     */
182    @Override
183    void layout(int delta, boolean animate) {
184        int childrenLeft = mSpinnerPadding.left;
185        int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
186
187        if (mDataChanged) {
188            handleDataChanged();
189        }
190
191        // Handle the empty set by removing all views
192        if (mItemCount == 0) {
193            resetList();
194            return;
195        }
196
197        if (mNextSelectedPosition >= 0) {
198            setSelectedPositionInt(mNextSelectedPosition);
199        }
200
201        recycleAllViews();
202
203        // Clear out old views
204        removeAllViewsInLayout();
205
206        // Make selected view and center it
207        mFirstPosition = mSelectedPosition;
208        View sel = makeAndAddView(mSelectedPosition);
209        int width = sel.getMeasuredWidth();
210        int selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
211        sel.offsetLeftAndRight(selectedOffset);
212
213        // Flush any cached views that did not get reused above
214        mRecycler.clear();
215
216        invalidate();
217
218        checkSelectionChanged();
219
220        mDataChanged = false;
221        mNeedSync = false;
222        setNextSelectedPositionInt(mSelectedPosition);
223    }
224
225    /**
226     * Obtain a view, either by pulling an existing view from the recycler or
227     * by getting a new one from the adapter. If we are animating, make sure
228     * there is enough information in the view's layout parameters to animate
229     * from the old to new positions.
230     *
231     * @param position Position in the spinner for the view to obtain
232     * @return A view that has been added to the spinner
233     */
234    private View makeAndAddView(int position) {
235
236        View child;
237
238        if (!mDataChanged) {
239            child = mRecycler.get(position);
240            if (child != null) {
241                // Position the view
242                setUpChild(child);
243
244                return child;
245            }
246        }
247
248        // Nothing found in the recycler -- ask the adapter for a view
249        child = mAdapter.getView(position, null, this);
250
251        // Position the view
252        setUpChild(child);
253
254        return child;
255    }
256
257    /**
258     * Helper for makeAndAddView to set the position of a view
259     * and fill out its layout paramters.
260     *
261     * @param child The view to position
262     */
263    private void setUpChild(View child) {
264
265        // Respect layout params that are already in the view. Otherwise
266        // make some up...
267        ViewGroup.LayoutParams lp = child.getLayoutParams();
268        if (lp == null) {
269            lp = generateDefaultLayoutParams();
270        }
271
272        addViewInLayout(child, 0, lp);
273
274        child.setSelected(hasFocus());
275
276        // Get measure specs
277        int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
278                mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
279        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
280                mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
281
282        // Measure child
283        child.measure(childWidthSpec, childHeightSpec);
284
285        int childLeft;
286        int childRight;
287
288        // Position vertically based on gravity setting
289        int childTop = mSpinnerPadding.top
290                + ((mMeasuredHeight - mSpinnerPadding.bottom -
291                        mSpinnerPadding.top - child.getMeasuredHeight()) / 2);
292        int childBottom = childTop + child.getMeasuredHeight();
293
294        int width = child.getMeasuredWidth();
295        childLeft = 0;
296        childRight = childLeft + width;
297
298        child.layout(childLeft, childTop, childRight, childBottom);
299    }
300
301    @Override
302    public boolean performClick() {
303        boolean handled = super.performClick();
304
305        if (!handled) {
306            handled = true;
307
308            if (!mPopup.isShowing()) {
309                mPopup.show();
310            }
311        }
312
313        return handled;
314    }
315
316    public void onClick(DialogInterface dialog, int which) {
317        setSelection(which);
318        dialog.dismiss();
319        mPopup = null;
320    }
321
322    /**
323     * Sets the prompt to display when the dialog is shown.
324     * @param prompt the prompt to set
325     */
326    public void setPrompt(CharSequence prompt) {
327        mPopup.setPromptText(prompt);
328    }
329
330    /**
331     * Sets the prompt to display when the dialog is shown.
332     * @param promptId the resource ID of the prompt to display when the dialog is shown
333     */
334    public void setPromptId(int promptId) {
335        setPrompt(getContext().getText(promptId));
336    }
337
338    /**
339     * @return The prompt to display when the dialog is shown
340     */
341    public CharSequence getPrompt() {
342        return mPopup.getHintText();
343    }
344
345    /**
346     * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
347     * into a ListAdapter.</p>
348     */
349    private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
350        private SpinnerAdapter mAdapter;
351        private ListAdapter mListAdapter;
352
353        /**
354         * <p>Creates a new ListAdapter wrapper for the specified adapter.</p>
355         *
356         * @param adapter the Adapter to transform into a ListAdapter
357         */
358        public DropDownAdapter(SpinnerAdapter adapter) {
359            this.mAdapter = adapter;
360            if (adapter instanceof ListAdapter) {
361                this.mListAdapter = (ListAdapter) adapter;
362            }
363        }
364
365        public int getCount() {
366            return mAdapter == null ? 0 : mAdapter.getCount();
367        }
368
369        public Object getItem(int position) {
370            return mAdapter == null ? null : mAdapter.getItem(position);
371        }
372
373        public long getItemId(int position) {
374            return mAdapter == null ? -1 : mAdapter.getItemId(position);
375        }
376
377        public View getView(int position, View convertView, ViewGroup parent) {
378            return getDropDownView(position, convertView, parent);
379        }
380
381        public View getDropDownView(int position, View convertView, ViewGroup parent) {
382            return mAdapter == null ? null :
383                    mAdapter.getDropDownView(position, convertView, parent);
384        }
385
386        public boolean hasStableIds() {
387            return mAdapter != null && mAdapter.hasStableIds();
388        }
389
390        public void registerDataSetObserver(DataSetObserver observer) {
391            if (mAdapter != null) {
392                mAdapter.registerDataSetObserver(observer);
393            }
394        }
395
396        public void unregisterDataSetObserver(DataSetObserver observer) {
397            if (mAdapter != null) {
398                mAdapter.unregisterDataSetObserver(observer);
399            }
400        }
401
402        /**
403         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
404         * Otherwise, return true.
405         */
406        public boolean areAllItemsEnabled() {
407            final ListAdapter adapter = mListAdapter;
408            if (adapter != null) {
409                return adapter.areAllItemsEnabled();
410            } else {
411                return true;
412            }
413        }
414
415        /**
416         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
417         * Otherwise, return true.
418         */
419        public boolean isEnabled(int position) {
420            final ListAdapter adapter = mListAdapter;
421            if (adapter != null) {
422                return adapter.isEnabled(position);
423            } else {
424                return true;
425            }
426        }
427
428        public int getItemViewType(int position) {
429            return 0;
430        }
431
432        public int getViewTypeCount() {
433            return 1;
434        }
435
436        public boolean isEmpty() {
437            return getCount() == 0;
438        }
439    }
440
441    /**
442     * Implements some sort of popup selection interface for selecting a spinner option.
443     * Allows for different spinner modes.
444     */
445    private interface SpinnerPopup {
446        public void setAdapter(ListAdapter adapter);
447
448        /**
449         * Show the popup
450         */
451        public void show();
452
453        /**
454         * Dismiss the popup
455         */
456        public void dismiss();
457
458        /**
459         * @return true if the popup is showing, false otherwise.
460         */
461        public boolean isShowing();
462
463        /**
464         * Set hint text to be displayed to the user. This should provide
465         * a description of the choice being made.
466         * @param hintText Hint text to set.
467         */
468        public void setPromptText(CharSequence hintText);
469        public CharSequence getHintText();
470    }
471
472    private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
473        private AlertDialog mPopup;
474        private ListAdapter mListAdapter;
475        private CharSequence mPrompt;
476
477        public void dismiss() {
478            mPopup.dismiss();
479            mPopup = null;
480        }
481
482        public boolean isShowing() {
483            return mPopup != null ? mPopup.isShowing() : false;
484        }
485
486        public void setAdapter(ListAdapter adapter) {
487            mListAdapter = adapter;
488        }
489
490        public void setPromptText(CharSequence hintText) {
491            mPrompt = hintText;
492        }
493
494        public CharSequence getHintText() {
495            return mPrompt;
496        }
497
498        public void show() {
499            AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
500            if (mPrompt != null) {
501                builder.setTitle(mPrompt);
502            }
503            mPopup = builder.setSingleChoiceItems(mListAdapter,
504                    getSelectedItemPosition(), this).show();
505        }
506
507        public void onClick(DialogInterface dialog, int which) {
508            setSelection(which);
509            dismiss();
510        }
511    }
512
513    private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
514        private CharSequence mHintText;
515        private TextView mHintView;
516        private int mHintResource;
517
518        public DropdownPopup(Context context, AttributeSet attrs,
519                int defStyleRes, int hintResource) {
520            super(context, attrs, 0, defStyleRes);
521
522            mHintResource = hintResource;
523
524            setAnchorView(Spinner.this);
525            setModal(true);
526            setPromptPosition(POSITION_PROMPT_BELOW);
527            setOnItemClickListener(new OnItemClickListener() {
528                public void onItemClick(AdapterView parent, View v, int position, long id) {
529                    Spinner.this.setSelection(position);
530                    dismiss();
531                }
532            });
533        }
534
535        public CharSequence getHintText() {
536            return mHintText;
537        }
538
539        public void setPromptText(CharSequence hintText) {
540            mHintText = hintText;
541            if (mHintView != null) {
542                mHintView.setText(hintText);
543            }
544        }
545
546        public void show() {
547            if (mHintView == null) {
548                final TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(
549                        mHintResource, null).findViewById(com.android.internal.R.id.text1);
550                textView.setText(mHintText);
551                setPromptView(textView);
552                mHintView = textView;
553            }
554            super.show();
555            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
556            setSelection(Spinner.this.getSelectedItemPosition());
557        }
558    }
559}
560