Home | History | Annotate | Download | only in widget
      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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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
    308     public void setPopupBackgroundResource(@DrawableRes int resId) {
    309         setPopupBackgroundDrawable(AppCompatResources.getDrawable(getPopupContext(), resId));
    310     }
    311 
    312     @Override
    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
    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
    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
    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
    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
    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
    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
    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
    403     protected void onDetachedFromWindow() {
    404         super.onDetachedFromWindow();
    405 
    406         if (mPopup != null && mPopup.isShowing()) {
    407             mPopup.dismiss();
    408         }
    409     }
    410 
    411     @Override
    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
    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
    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
    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
    456     public CharSequence getPrompt() {
    457         return mPopup != null ? mPopup.getHintText() : super.getPrompt();
    458     }
    459 
    460     @Override
    461     public void setBackgroundResource(@DrawableRes int resId) {
    462         super.setBackgroundResource(resId);
    463         if (mBackgroundTintHelper != null) {
    464             mBackgroundTintHelper.onSetBackgroundResource(resId);
    465         }
    466     }
    467 
    468     @Override
    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
    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
    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
    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
    529     public PorterDuff.Mode getSupportBackgroundTintMode() {
    530         return mBackgroundTintHelper != null
    531                 ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
    532     }
    533 
    534     @Override
    535     protected void drawableStateChanged() {
    536         super.drawableStateChanged();
    537         if (mBackgroundTintHelper != null) {
    538             mBackgroundTintHelper.applySupportBackgroundTint();
    539         }
    540     }
    541 
    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          */
    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
    629         public int getCount() {
    630             return mAdapter == null ? 0 : mAdapter.getCount();
    631         }
    632 
    633         @Override
    634         public Object getItem(int position) {
    635             return mAdapter == null ? null : mAdapter.getItem(position);
    636         }
    637 
    638         @Override
    639         public long getItemId(int position) {
    640             return mAdapter == null ? -1 : mAdapter.getItemId(position);
    641         }
    642 
    643         @Override
    644         public View getView(int position, View convertView, ViewGroup parent) {
    645             return getDropDownView(position, convertView, parent);
    646         }
    647 
    648         @Override
    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
    655         public boolean hasStableIds() {
    656             return mAdapter != null && mAdapter.hasStableIds();
    657         }
    658 
    659         @Override
    660         public void registerDataSetObserver(DataSetObserver observer) {
    661             if (mAdapter != null) {
    662                 mAdapter.registerDataSetObserver(observer);
    663             }
    664         }
    665 
    666         @Override
    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
    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
    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
    702         public int getItemViewType(int position) {
    703             return 0;
    704         }
    705 
    706         @Override
    707         public int getViewTypeCount() {
    708             return 1;
    709         }
    710 
    711         @Override
    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 
    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
    743         public void setAdapter(ListAdapter adapter) {
    744             super.setAdapter(adapter);
    745             mAdapter = adapter;
    746         }
    747 
    748         public CharSequence getHintText() {
    749             return mHintText;
    750         }
    751 
    752         public void setPromptText(CharSequence hintText) {
    753             // Hint text is ignored for dropdowns, but maintain it here.
    754             mHintText = hintText;
    755         }
    756 
    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
    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          */
    848         boolean isVisibleToUser(View view) {
    849             return ViewCompat.isAttachedToWindow(view) && view.getGlobalVisibleRect(mVisibleRect);
    850         }
    851     }
    852 }
    853