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