1/*
2 * Copyright (C) 2014 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.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.database.DataSetObserver;
24import android.graphics.PorterDuff;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.os.Build;
28import android.support.annotation.DrawableRes;
29import android.support.annotation.Nullable;
30import android.support.v4.content.ContextCompat;
31import android.support.v4.view.TintableBackgroundView;
32import android.support.v4.view.ViewCompat;
33import android.support.v7.appcompat.R;
34import android.support.v7.view.ContextThemeWrapper;
35import android.support.v7.view.menu.ShowableListMenu;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.ViewGroup;
41import android.view.ViewTreeObserver;
42import android.widget.AdapterView;
43import android.widget.ArrayAdapter;
44import android.widget.ListAdapter;
45import android.widget.ListView;
46import android.widget.PopupWindow;
47import android.widget.Spinner;
48import android.widget.SpinnerAdapter;
49
50
51/**
52 * A {@link Spinner} which supports compatible features on older versions of the platform,
53 * including:
54 * <ul>
55 * <li>Dynamic tinting of the background via the background tint methods in
56 * {@link android.support.v4.view.ViewCompat}.</li>
57 * <li>Configuring the background tint using {@link R.attr#backgroundTint} and
58 * {@link R.attr#backgroundTintMode}.</li>
59 * <li>Setting the popup theme using {@link R.attr#popupTheme}.</li>
60 * </ul>
61 *
62 * <p>This will automatically be used when you use {@link Spinner} in your layouts.
63 * You should only need to manually use this class when writing custom views.</p>
64 */
65public class AppCompatSpinner extends Spinner implements TintableBackgroundView {
66
67    private static final boolean IS_AT_LEAST_M = Build.VERSION.SDK_INT >= 23;
68    private static final boolean IS_AT_LEAST_JB = Build.VERSION.SDK_INT >= 16;
69
70    private static final int[] ATTRS_ANDROID_SPINNERMODE = {android.R.attr.spinnerMode};
71
72    private static final int MAX_ITEMS_MEASURED = 15;
73
74    private static final String TAG = "AppCompatSpinner";
75
76    private static final int MODE_DIALOG = 0;
77    private static final int MODE_DROPDOWN = 1;
78    private static final int MODE_THEME = -1;
79
80    private AppCompatDrawableManager mDrawableManager;
81
82    private AppCompatBackgroundHelper mBackgroundTintHelper;
83
84    /** Context used to inflate the popup window or dialog. */
85    private Context mPopupContext;
86
87    /** Forwarding listener used to implement drag-to-open. */
88    private ForwardingListener mForwardingListener;
89
90    /** Temporary holder for setAdapter() calls from the super constructor. */
91    private SpinnerAdapter mTempAdapter;
92
93    private boolean mPopupSet;
94
95    private DropdownPopup mPopup;
96
97    private int mDropDownWidth;
98
99    private final Rect mTempRect = new Rect();
100
101    /**
102     * Construct a new spinner with the given context's theme.
103     *
104     * @param context The Context the view is running in, through which it can
105     *                access the current theme, resources, etc.
106     */
107    public AppCompatSpinner(Context context) {
108        this(context, null);
109    }
110
111    /**
112     * Construct a new spinner with the given context's theme and the supplied
113     * mode of displaying choices. <code>mode</code> may be one of
114     * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN}.
115     *
116     * @param context The Context the view is running in, through which it can
117     *                access the current theme, resources, etc.
118     * @param mode    Constant describing how the user will select choices from the spinner.
119     * @see #MODE_DIALOG
120     * @see #MODE_DROPDOWN
121     */
122    public AppCompatSpinner(Context context, int mode) {
123        this(context, null, R.attr.spinnerStyle, mode);
124    }
125
126    /**
127     * Construct a new spinner with the given context's theme and the supplied attribute set.
128     *
129     * @param context The Context the view is running in, through which it can
130     *                access the current theme, resources, etc.
131     * @param attrs   The attributes of the XML tag that is inflating the view.
132     */
133    public AppCompatSpinner(Context context, AttributeSet attrs) {
134        this(context, attrs, R.attr.spinnerStyle);
135    }
136
137    /**
138     * Construct a new spinner with the given context's theme, the supplied attribute set,
139     * and default style attribute.
140     *
141     * @param context      The Context the view is running in, through which it can
142     *                     access the current theme, resources, etc.
143     * @param attrs        The attributes of the XML tag that is inflating the view.
144     * @param defStyleAttr An attribute in the current theme that contains a
145     *                     reference to a style resource that supplies default values for
146     *                     the view. Can be 0 to not look for defaults.
147     */
148    public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
149        this(context, attrs, defStyleAttr, MODE_THEME);
150    }
151
152    /**
153     * Construct a new spinner with the given context's theme, the supplied attribute set,
154     * and default style. <code>mode</code> may be one of {@link #MODE_DIALOG} or
155     * {@link #MODE_DROPDOWN} and determines how the user will select choices from the spinner.
156     *
157     * @param context      The Context the view is running in, through which it can
158     *                     access the current theme, resources, etc.
159     * @param attrs        The attributes of the XML tag that is inflating the view.
160     * @param defStyleAttr An attribute in the current theme that contains a
161     *                     reference to a style resource that supplies default values for
162     *                     the view. Can be 0 to not look for defaults.
163     * @param mode         Constant describing how the user will select choices from the spinner.
164     * @see #MODE_DIALOG
165     * @see #MODE_DROPDOWN
166     */
167    public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
168        this(context, attrs, defStyleAttr, mode, null);
169    }
170
171
172    /**
173     * Constructs a new spinner with the given context's theme, the supplied
174     * attribute set, default styles, popup mode (one of {@link #MODE_DIALOG}
175     * or {@link #MODE_DROPDOWN}), and the context against which the popup
176     * should be inflated.
177     *
178     * @param context      The context against which the view is inflated, which
179     *                     provides access to the current theme, resources, etc.
180     * @param attrs        The attributes of the XML tag that is inflating the view.
181     * @param defStyleAttr An attribute in the current theme that contains a
182     *                     reference to a style resource that supplies default
183     *                     values for the view. Can be 0 to not look for
184     *                     defaults.
185     * @param mode         Constant describing how the user will select choices from
186     *                     the spinner.
187     * @param popupTheme   The theme against which the dialog or dropdown popup
188     *                     should be inflated. May be {@code null} to use the
189     *                     view theme. If set, this will override any value
190     *                     specified by
191     *                     {@link R.styleable#Spinner_popupTheme}.
192     * @see #MODE_DIALOG
193     * @see #MODE_DROPDOWN
194     */
195    public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode,
196            Resources.Theme popupTheme) {
197        super(context, attrs, defStyleAttr);
198
199        TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
200                R.styleable.Spinner, defStyleAttr, 0);
201
202        mDrawableManager = AppCompatDrawableManager.get();
203        mBackgroundTintHelper = new AppCompatBackgroundHelper(this, mDrawableManager);
204
205        if (popupTheme != null) {
206            mPopupContext = new ContextThemeWrapper(context, popupTheme);
207        } else {
208            final int popupThemeResId = a.getResourceId(R.styleable.Spinner_popupTheme, 0);
209            if (popupThemeResId != 0) {
210                mPopupContext = new ContextThemeWrapper(context, popupThemeResId);
211            } else {
212                // If we're running on a < M device, we'll use the current context and still handle
213                // any dropdown popup
214                mPopupContext = !IS_AT_LEAST_M ? context : null;
215            }
216        }
217
218        if (mPopupContext != null) {
219            if (mode == MODE_THEME) {
220                if (Build.VERSION.SDK_INT >= 11) {
221                    // If we're running on API v11+ we will try and read android:spinnerMode
222                    TypedArray aa = null;
223                    try {
224                        aa = context.obtainStyledAttributes(attrs, ATTRS_ANDROID_SPINNERMODE,
225                                defStyleAttr, 0);
226                        if (aa.hasValue(0)) {
227                            mode = aa.getInt(0, MODE_DIALOG);
228                        }
229                    } catch (Exception e) {
230                        Log.i(TAG, "Could not read android:spinnerMode", e);
231                    } finally {
232                        if (aa != null) {
233                            aa.recycle();
234                        }
235                    }
236                } else {
237                    // Else, we use a default mode of dropdown
238                    mode = MODE_DROPDOWN;
239                }
240            }
241
242            if (mode == MODE_DROPDOWN) {
243                final DropdownPopup popup = new DropdownPopup(mPopupContext, attrs, defStyleAttr);
244                final TintTypedArray pa = TintTypedArray.obtainStyledAttributes(
245                        mPopupContext, attrs, R.styleable.Spinner, defStyleAttr, 0);
246                mDropDownWidth = pa.getLayoutDimension(R.styleable.Spinner_android_dropDownWidth,
247                        LayoutParams.WRAP_CONTENT);
248                popup.setBackgroundDrawable(
249                        pa.getDrawable(R.styleable.Spinner_android_popupBackground));
250                popup.setPromptText(a.getString(R.styleable.Spinner_android_prompt));
251                pa.recycle();
252
253                mPopup = popup;
254                mForwardingListener = new ForwardingListener(this) {
255                    @Override
256                    public ShowableListMenu getPopup() {
257                        return popup;
258                    }
259
260                    @Override
261                    public boolean onForwardingStarted() {
262                        if (!mPopup.isShowing()) {
263                            mPopup.show();
264                        }
265                        return true;
266                    }
267                };
268            }
269        }
270
271        final CharSequence[] entries = a.getTextArray(R.styleable.Spinner_android_entries);
272        if (entries != null) {
273            final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<>(
274                    context, android.R.layout.simple_spinner_item, entries);
275            adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item);
276            setAdapter(adapter);
277        }
278
279        a.recycle();
280
281        mPopupSet = true;
282
283        // Base constructors can call setAdapter before we initialize mPopup.
284        // Finish setting things up if this happened.
285        if (mTempAdapter != null) {
286            setAdapter(mTempAdapter);
287            mTempAdapter = null;
288        }
289
290        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
291    }
292
293    /**
294     * @return the context used to inflate the Spinner's popup or dialog window
295     */
296    public Context getPopupContext() {
297        if (mPopup != null) {
298            return mPopupContext;
299        } else if (IS_AT_LEAST_M) {
300            return super.getPopupContext();
301        }
302        return null;
303    }
304
305    public void setPopupBackgroundDrawable(Drawable background) {
306        if (mPopup != null) {
307            mPopup.setBackgroundDrawable(background);
308        } else if (IS_AT_LEAST_JB) {
309            super.setPopupBackgroundDrawable(background);
310        }
311    }
312
313    public void setPopupBackgroundResource(@DrawableRes int resId) {
314        setPopupBackgroundDrawable(ContextCompat.getDrawable(getPopupContext(), resId));
315    }
316
317    public Drawable getPopupBackground() {
318        if (mPopup != null) {
319            return mPopup.getBackground();
320        } else if (IS_AT_LEAST_JB) {
321            return super.getPopupBackground();
322        }
323        return null;
324    }
325
326    public void setDropDownVerticalOffset(int pixels) {
327        if (mPopup != null) {
328            mPopup.setVerticalOffset(pixels);
329        } else if (IS_AT_LEAST_JB) {
330            super.setDropDownVerticalOffset(pixels);
331        }
332    }
333
334    public int getDropDownVerticalOffset() {
335        if (mPopup != null) {
336            return mPopup.getVerticalOffset();
337        } else if (IS_AT_LEAST_JB) {
338            return super.getDropDownVerticalOffset();
339        }
340        return 0;
341    }
342
343    public void setDropDownHorizontalOffset(int pixels) {
344        if (mPopup != null) {
345            mPopup.setHorizontalOffset(pixels);
346        } else if (IS_AT_LEAST_JB) {
347            super.setDropDownHorizontalOffset(pixels);
348        }
349    }
350
351    /**
352     * Get the configured horizontal offset in pixels for the spinner's popup window of choices.
353     * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
354     *
355     * @return Horizontal offset in pixels
356     */
357    public int getDropDownHorizontalOffset() {
358        if (mPopup != null) {
359            return mPopup.getHorizontalOffset();
360        } else if (IS_AT_LEAST_JB) {
361            return super.getDropDownHorizontalOffset();
362        }
363        return 0;
364    }
365
366    public void setDropDownWidth(int pixels) {
367        if (mPopup != null) {
368            mDropDownWidth = pixels;
369        } else if (IS_AT_LEAST_JB) {
370            super.setDropDownWidth(pixels);
371        }
372    }
373
374    public int getDropDownWidth() {
375        if (mPopup != null) {
376            return mDropDownWidth;
377        } else if (IS_AT_LEAST_JB) {
378            return super.getDropDownWidth();
379        }
380        return 0;
381    }
382
383    @Override
384    public void setAdapter(SpinnerAdapter adapter) {
385        // The super constructor may call setAdapter before we're prepared.
386        // Postpone doing anything until we've finished construction.
387        if (!mPopupSet) {
388            mTempAdapter = adapter;
389            return;
390        }
391
392        super.setAdapter(adapter);
393
394        if (mPopup != null) {
395            final Context popupContext = mPopupContext == null ? getContext() : mPopupContext;
396            mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme()));
397        }
398    }
399
400    @Override
401    protected void onDetachedFromWindow() {
402        super.onDetachedFromWindow();
403
404        if (mPopup != null && mPopup.isShowing()) {
405            mPopup.dismiss();
406        }
407    }
408
409    @Override
410    public boolean onTouchEvent(MotionEvent event) {
411        if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) {
412            return true;
413        }
414        return super.onTouchEvent(event);
415    }
416
417    @Override
418    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
419        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
420
421        if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
422            final int measuredWidth = getMeasuredWidth();
423            setMeasuredDimension(Math.min(Math.max(measuredWidth,
424                                    compatMeasureContentWidth(getAdapter(), getBackground())),
425                            MeasureSpec.getSize(widthMeasureSpec)),
426                    getMeasuredHeight());
427        }
428    }
429
430    @Override
431    public boolean performClick() {
432        if (mPopup != null) {
433            // If we have a popup, show it if needed, or just consume the click...
434            if (!mPopup.isShowing()) {
435                mPopup.show();
436            }
437            return true;
438        }
439
440        // Else let the platform handle the click
441        return super.performClick();
442    }
443
444    @Override
445    public void setPrompt(CharSequence prompt) {
446        if (mPopup != null) {
447            mPopup.setPromptText(prompt);
448        } else {
449            super.setPrompt(prompt);
450        }
451    }
452
453    @Override
454    public CharSequence getPrompt() {
455        return mPopup != null ? mPopup.getHintText() : super.getPrompt();
456    }
457
458    @Override
459    public void setBackgroundResource(@DrawableRes int resId) {
460        super.setBackgroundResource(resId);
461        if (mBackgroundTintHelper != null) {
462            mBackgroundTintHelper.onSetBackgroundResource(resId);
463        }
464    }
465
466    @Override
467    public void setBackgroundDrawable(Drawable background) {
468        super.setBackgroundDrawable(background);
469        if (mBackgroundTintHelper != null) {
470            mBackgroundTintHelper.onSetBackgroundDrawable(background);
471        }
472    }
473
474    /**
475     * This should be accessed via
476     * {@link android.support.v4.view.ViewCompat#setBackgroundTintList(android.view.View,
477     * ColorStateList)}
478     *
479     * @hide
480     */
481    @Override
482    public void setSupportBackgroundTintList(@Nullable ColorStateList tint) {
483        if (mBackgroundTintHelper != null) {
484            mBackgroundTintHelper.setSupportBackgroundTintList(tint);
485        }
486    }
487
488    /**
489     * This should be accessed via
490     * {@link android.support.v4.view.ViewCompat#getBackgroundTintList(android.view.View)}
491     *
492     * @hide
493     */
494    @Override
495    @Nullable
496    public ColorStateList getSupportBackgroundTintList() {
497        return mBackgroundTintHelper != null
498                ? mBackgroundTintHelper.getSupportBackgroundTintList() : null;
499    }
500
501    /**
502     * This should be accessed via
503     * {@link android.support.v4.view.ViewCompat#setBackgroundTintMode(android.view.View,
504     * PorterDuff.Mode)}
505     *
506     * @hide
507     */
508    @Override
509    public void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
510        if (mBackgroundTintHelper != null) {
511            mBackgroundTintHelper.setSupportBackgroundTintMode(tintMode);
512        }
513    }
514
515    /**
516     * This should be accessed via
517     * {@link android.support.v4.view.ViewCompat#getBackgroundTintMode(android.view.View)}
518     *
519     * @hide
520     */
521    @Override
522    @Nullable
523    public PorterDuff.Mode getSupportBackgroundTintMode() {
524        return mBackgroundTintHelper != null
525                ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
526    }
527
528    @Override
529    protected void drawableStateChanged() {
530        super.drawableStateChanged();
531        if (mBackgroundTintHelper != null) {
532            mBackgroundTintHelper.applySupportBackgroundTint();
533        }
534    }
535
536    private int compatMeasureContentWidth(SpinnerAdapter adapter, Drawable background) {
537        if (adapter == null) {
538            return 0;
539        }
540
541        int width = 0;
542        View itemView = null;
543        int itemType = 0;
544        final int widthMeasureSpec =
545                MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.UNSPECIFIED);
546        final int heightMeasureSpec =
547                MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED);
548
549        // Make sure the number of items we'll measure is capped. If it's a huge data set
550        // with wildly varying sizes, oh well.
551        int start = Math.max(0, getSelectedItemPosition());
552        final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
553        final int count = end - start;
554        start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
555        for (int i = start; i < end; i++) {
556            final int positionType = adapter.getItemViewType(i);
557            if (positionType != itemType) {
558                itemType = positionType;
559                itemView = null;
560            }
561            itemView = adapter.getView(i, itemView, this);
562            if (itemView.getLayoutParams() == null) {
563                itemView.setLayoutParams(new LayoutParams(
564                        LayoutParams.WRAP_CONTENT,
565                        LayoutParams.WRAP_CONTENT));
566            }
567            itemView.measure(widthMeasureSpec, heightMeasureSpec);
568            width = Math.max(width, itemView.getMeasuredWidth());
569        }
570
571        // Add background padding to measured width
572        if (background != null) {
573            background.getPadding(mTempRect);
574            width += mTempRect.left + mTempRect.right;
575        }
576
577        return width;
578    }
579
580    /**
581     * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
582     * into a ListAdapter.</p>
583     */
584    private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
585
586        private SpinnerAdapter mAdapter;
587
588        private ListAdapter mListAdapter;
589
590        /**
591         * Creates a new ListAdapter wrapper for the specified adapter.
592         *
593         * @param adapter       the SpinnerAdapter to transform into a ListAdapter
594         * @param dropDownTheme the theme against which to inflate drop-down
595         *                      views, may be {@null} to use default theme
596         */
597        public DropDownAdapter(@Nullable SpinnerAdapter adapter,
598                @Nullable Resources.Theme dropDownTheme) {
599            mAdapter = adapter;
600
601            if (adapter instanceof ListAdapter) {
602                mListAdapter = (ListAdapter) adapter;
603            }
604
605            if (dropDownTheme != null) {
606                 if (IS_AT_LEAST_M && adapter instanceof android.widget.ThemedSpinnerAdapter) {
607                    final android.widget.ThemedSpinnerAdapter themedAdapter =
608                            (android.widget.ThemedSpinnerAdapter) adapter;
609                    if (themedAdapter.getDropDownViewTheme() != dropDownTheme) {
610                        themedAdapter.setDropDownViewTheme(dropDownTheme);
611                    }
612                } else if (adapter instanceof ThemedSpinnerAdapter) {
613                    final ThemedSpinnerAdapter themedAdapter = (ThemedSpinnerAdapter) adapter;
614                    if (themedAdapter.getDropDownViewTheme() == null) {
615                        themedAdapter.setDropDownViewTheme(dropDownTheme);
616                    }
617                }
618            }
619        }
620
621        public int getCount() {
622            return mAdapter == null ? 0 : mAdapter.getCount();
623        }
624
625        public Object getItem(int position) {
626            return mAdapter == null ? null : mAdapter.getItem(position);
627        }
628
629        public long getItemId(int position) {
630            return mAdapter == null ? -1 : mAdapter.getItemId(position);
631        }
632
633        public View getView(int position, View convertView, ViewGroup parent) {
634            return getDropDownView(position, convertView, parent);
635        }
636
637        public View getDropDownView(int position, View convertView, ViewGroup parent) {
638            return (mAdapter == null) ? null
639                    : mAdapter.getDropDownView(position, convertView, parent);
640        }
641
642        public boolean hasStableIds() {
643            return mAdapter != null && mAdapter.hasStableIds();
644        }
645
646        public void registerDataSetObserver(DataSetObserver observer) {
647            if (mAdapter != null) {
648                mAdapter.registerDataSetObserver(observer);
649            }
650        }
651
652        public void unregisterDataSetObserver(DataSetObserver observer) {
653            if (mAdapter != null) {
654                mAdapter.unregisterDataSetObserver(observer);
655            }
656        }
657
658        /**
659         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
660         * Otherwise, return true.
661         */
662        public boolean areAllItemsEnabled() {
663            final ListAdapter adapter = mListAdapter;
664            if (adapter != null) {
665                return adapter.areAllItemsEnabled();
666            } else {
667                return true;
668            }
669        }
670
671        /**
672         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
673         * Otherwise, return true.
674         */
675        public boolean isEnabled(int position) {
676            final ListAdapter adapter = mListAdapter;
677            if (adapter != null) {
678                return adapter.isEnabled(position);
679            } else {
680                return true;
681            }
682        }
683
684        public int getItemViewType(int position) {
685            return 0;
686        }
687
688        public int getViewTypeCount() {
689            return 1;
690        }
691
692        public boolean isEmpty() {
693            return getCount() == 0;
694        }
695    }
696
697    private class DropdownPopup extends ListPopupWindow {
698        private CharSequence mHintText;
699        private ListAdapter mAdapter;
700        private final Rect mVisibleRect = new Rect();
701
702        public DropdownPopup(Context context, AttributeSet attrs, int defStyleAttr) {
703            super(context, attrs, defStyleAttr);
704
705            setAnchorView(AppCompatSpinner.this);
706            setModal(true);
707            setPromptPosition(POSITION_PROMPT_ABOVE);
708
709            setOnItemClickListener(new AdapterView.OnItemClickListener() {
710                @Override
711                public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
712                    AppCompatSpinner.this.setSelection(position);
713                    if (getOnItemClickListener() != null) {
714                        AppCompatSpinner.this
715                                .performItemClick(v, position, mAdapter.getItemId(position));
716                    }
717                    dismiss();
718                }
719            });
720        }
721
722        @Override
723        public void setAdapter(ListAdapter adapter) {
724            super.setAdapter(adapter);
725            mAdapter = adapter;
726        }
727
728        public CharSequence getHintText() {
729            return mHintText;
730        }
731
732        public void setPromptText(CharSequence hintText) {
733            // Hint text is ignored for dropdowns, but maintain it here.
734            mHintText = hintText;
735        }
736
737        void computeContentWidth() {
738            final Drawable background = getBackground();
739            int hOffset = 0;
740            if (background != null) {
741                background.getPadding(mTempRect);
742                hOffset = ViewUtils.isLayoutRtl(AppCompatSpinner.this) ? mTempRect.right
743                        : -mTempRect.left;
744            } else {
745                mTempRect.left = mTempRect.right = 0;
746            }
747
748            final int spinnerPaddingLeft = AppCompatSpinner.this.getPaddingLeft();
749            final int spinnerPaddingRight = AppCompatSpinner.this.getPaddingRight();
750            final int spinnerWidth = AppCompatSpinner.this.getWidth();
751            if (mDropDownWidth == WRAP_CONTENT) {
752                int contentWidth = compatMeasureContentWidth(
753                        (SpinnerAdapter) mAdapter, getBackground());
754                final int contentWidthLimit = getContext().getResources()
755                        .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
756                if (contentWidth > contentWidthLimit) {
757                    contentWidth = contentWidthLimit;
758                }
759                setContentWidth(Math.max(
760                        contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
761            } else if (mDropDownWidth == MATCH_PARENT) {
762                setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
763            } else {
764                setContentWidth(mDropDownWidth);
765            }
766            if (ViewUtils.isLayoutRtl(AppCompatSpinner.this)) {
767                hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
768            } else {
769                hOffset += spinnerPaddingLeft;
770            }
771            setHorizontalOffset(hOffset);
772        }
773
774        public void show() {
775            final boolean wasShowing = isShowing();
776
777            computeContentWidth();
778
779            setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
780            super.show();
781            final ListView listView = getListView();
782            listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
783            setSelection(AppCompatSpinner.this.getSelectedItemPosition());
784
785            if (wasShowing) {
786                // Skip setting up the layout/dismiss listener below. If we were previously
787                // showing it will still stick around.
788                return;
789            }
790
791            // Make sure we hide if our anchor goes away.
792            // TODO: This might be appropriate to push all the way down to PopupWindow,
793            // but it may have other side effects to investigate first. (Text editing handles, etc.)
794            final ViewTreeObserver vto = getViewTreeObserver();
795            if (vto != null) {
796                final ViewTreeObserver.OnGlobalLayoutListener layoutListener
797                        = new ViewTreeObserver.OnGlobalLayoutListener() {
798                    @Override
799                    public void onGlobalLayout() {
800                        if (!isVisibleToUser(AppCompatSpinner.this)) {
801                            dismiss();
802                        } else {
803                            computeContentWidth();
804
805                            // Use super.show here to update; we don't want to move the selected
806                            // position or adjust other things that would be reset otherwise.
807                            DropdownPopup.super.show();
808                        }
809                    }
810                };
811                vto.addOnGlobalLayoutListener(layoutListener);
812                setOnDismissListener(new PopupWindow.OnDismissListener() {
813                    @Override
814                    public void onDismiss() {
815                        final ViewTreeObserver vto = getViewTreeObserver();
816                        if (vto != null) {
817                            vto.removeGlobalOnLayoutListener(layoutListener);
818                        }
819                    }
820                });
821            }
822        }
823
824        /**
825         * Simplified version of the the hidden View.isVisibleToUser()
826         */
827        private boolean isVisibleToUser(View view) {
828            return ViewCompat.isAttachedToWindow(view) && view.getGlobalVisibleRect(mVisibleRect);
829        }
830    }
831}
832