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