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