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