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