public class

AppCompatSpinner

extends Spinner

implements TintableBackgroundView

 java.lang.Object

↳Spinner

↳androidx.appcompat.widget.AppCompatSpinner

Gradle dependencies

compile group: 'androidx.appcompat', name: 'appcompat', version: '1.6.0-alpha04'

  • groupId: androidx.appcompat
  • artifactId: appcompat
  • version: 1.6.0-alpha04

Artifact androidx.appcompat:appcompat:1.6.0-alpha04 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.appcompat:appcompat com.android.support:appcompat-v7

Androidx class mapping:

androidx.appcompat.widget.AppCompatSpinner android.support.v7.widget.AppCompatSpinner

Overview

A which supports compatible features on older versions of the platform, including:

  • Allows dynamic tint of its background via the background tint methods in ViewCompat.
  • Allows setting of the background tint using and .
  • Setting the popup theme using .

This will automatically be used when you use in your layouts. You should only need to manually use this class when writing custom views.

Summary

Constructors
publicAppCompatSpinner(Context context)

Construct a new spinner with the given context's theme.

publicAppCompatSpinner(Context context, AttributeSet attrs)

Construct a new spinner with the given context's theme and the supplied attribute set.

publicAppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr)

Construct a new spinner with the given context's theme, the supplied attribute set, and default style attribute.

publicAppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode)

Construct a new spinner with the given context's theme, the supplied attribute set, and default style.

publicAppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode, Resources.Theme popupTheme)

Constructs a new spinner with the given context's theme, the supplied attribute set, default styles, popup mode (one of AppCompatSpinner.MODE_DIALOG or AppCompatSpinner.MODE_DROPDOWN), and the context against which the popup should be inflated.

publicAppCompatSpinner(Context context, int mode)

Construct a new spinner with the given context's theme and the supplied mode of displaying choices.

Methods
protected voiddrawableStateChanged()

public intgetDropDownHorizontalOffset()

Get the configured horizontal offset in pixels for the spinner's popup window of choices.

public intgetDropDownVerticalOffset()

public intgetDropDownWidth()

public DrawablegetPopupBackground()

public ContextgetPopupContext()

public java.lang.CharSequencegetPrompt()

public ColorStateListgetSupportBackgroundTintList()

This should be accessed via ViewCompat

public PorterDuff.ModegetSupportBackgroundTintMode()

This should be accessed via ViewCompat

protected voidonDetachedFromWindow()

protected voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)

public voidonRestoreInstanceState(Parcelable state)

public ParcelableonSaveInstanceState()

public booleanonTouchEvent(MotionEvent event)

public booleanperformClick()

public voidsetAdapter(SpinnerAdapter adapter)

public voidsetBackgroundDrawable(Drawable background)

public voidsetBackgroundResource(int resId)

public voidsetDropDownHorizontalOffset(int pixels)

public voidsetDropDownVerticalOffset(int pixels)

public voidsetDropDownWidth(int pixels)

public voidsetPopupBackgroundDrawable(Drawable background)

public voidsetPopupBackgroundResource(int resId)

public voidsetPrompt(java.lang.CharSequence prompt)

public voidsetSupportBackgroundTintList(ColorStateList tint)

This should be accessed via ViewCompat

public voidsetSupportBackgroundTintMode(PorterDuff.Mode tintMode)

This should be accessed via ViewCompat

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public AppCompatSpinner(Context context)

Construct a new spinner with the given context's theme.

Parameters:

context: The Context the view is running in, through which it can access the current theme, resources, etc.

public AppCompatSpinner(Context context, int mode)

Construct a new spinner with the given context's theme and the supplied mode of displaying choices. mode may be one of AppCompatSpinner.MODE_DIALOG or AppCompatSpinner.MODE_DROPDOWN.

Parameters:

context: The Context the view is running in, through which it can access the current theme, resources, etc.
mode: Constant describing how the user will select choices from the spinner.

See also: AppCompatSpinner.MODE_DIALOG, AppCompatSpinner.MODE_DROPDOWN

public AppCompatSpinner(Context context, AttributeSet attrs)

Construct a new spinner with the given context's theme and the supplied attribute set.

Parameters:

context: The Context the view is running in, through which it can access the current theme, resources, etc.
attrs: The attributes of the XML tag that is inflating the view.

public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr)

Construct a new spinner with the given context's theme, the supplied attribute set, and default style attribute.

Parameters:

context: The Context the view is running in, through which it can access the current theme, resources, etc.
attrs: The attributes of the XML tag that is inflating the view.
defStyleAttr: An attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.

public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode)

Construct a new spinner with the given context's theme, the supplied attribute set, and default style. mode may be one of AppCompatSpinner.MODE_DIALOG or AppCompatSpinner.MODE_DROPDOWN and determines how the user will select choices from the spinner.

Parameters:

context: The Context the view is running in, through which it can access the current theme, resources, etc.
attrs: The attributes of the XML tag that is inflating the view.
defStyleAttr: An attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.
mode: Constant describing how the user will select choices from the spinner.

See also: AppCompatSpinner.MODE_DIALOG, AppCompatSpinner.MODE_DROPDOWN

public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode, Resources.Theme popupTheme)

Constructs a new spinner with the given context's theme, the supplied attribute set, default styles, popup mode (one of AppCompatSpinner.MODE_DIALOG or AppCompatSpinner.MODE_DROPDOWN), and the context against which the popup should be inflated.

Parameters:

context: The context against which the view is inflated, which provides access to the current theme, resources, etc.
attrs: The attributes of the XML tag that is inflating the view.
defStyleAttr: An attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.
mode: Constant describing how the user will select choices from the spinner.
popupTheme: The theme against which the dialog or dropdown popup should be inflated. May be null to use the view theme. If set, this will override any value specified by .

See also: AppCompatSpinner.MODE_DIALOG, AppCompatSpinner.MODE_DROPDOWN

Methods

public Context getPopupContext()

Returns:

the context used to inflate the Spinner's popup or dialog window

public void setPopupBackgroundDrawable(Drawable background)

public void setPopupBackgroundResource(int resId)

public Drawable getPopupBackground()

public void setDropDownVerticalOffset(int pixels)

public int getDropDownVerticalOffset()

public void setDropDownHorizontalOffset(int pixels)

public int getDropDownHorizontalOffset()

Get the configured horizontal offset in pixels for the spinner's popup window of choices. Only valid in AppCompatSpinner.MODE_DROPDOWN; other modes will return 0.

Returns:

Horizontal offset in pixels

public void setDropDownWidth(int pixels)

public int getDropDownWidth()

public void setAdapter(SpinnerAdapter adapter)

protected void onDetachedFromWindow()

public boolean onTouchEvent(MotionEvent event)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

public boolean performClick()

public void setPrompt(java.lang.CharSequence prompt)

public java.lang.CharSequence getPrompt()

public void setBackgroundResource(int resId)

public void setBackgroundDrawable(Drawable background)

public void setSupportBackgroundTintList(ColorStateList tint)

This should be accessed via ViewCompat

public ColorStateList getSupportBackgroundTintList()

This should be accessed via ViewCompat

public void setSupportBackgroundTintMode(PorterDuff.Mode tintMode)

This should be accessed via ViewCompat

public PorterDuff.Mode getSupportBackgroundTintMode()

This should be accessed via ViewCompat

protected void drawableStateChanged()

public Parcelable onSaveInstanceState()

public void onRestoreInstanceState(Parcelable state)

Source

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.appcompat.widget;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.Spinner;
import android.widget.SpinnerAdapter;

import androidx.annotation.DoNotInline;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleableRes;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.R;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.view.menu.ShowableListMenu;
import androidx.core.util.ObjectsCompat;
import androidx.core.view.TintableBackgroundView;
import androidx.core.view.ViewCompat;
import androidx.resourceinspection.annotation.AppCompatShadowedAttributes;


/**
 * A {@link Spinner} which supports compatible features on older versions of the platform,
 * including:
 * <ul>
 *     <li>Allows dynamic tint of its background via the background tint methods in
 *     {@link androidx.core.view.ViewCompat}.</li>
 *     <li>Allows setting of the background tint using {@link R.attr#buttonTint} and
 *     {@link R.attr#buttonTintMode}.</li>
 *     <li>Setting the popup theme using {@link R.attr#popupTheme}.</li>
 * </ul>
 *
 * <p>This will automatically be used when you use {@link Spinner} in your layouts.
 * You should only need to manually use this class when writing custom views.</p>
 */
@AppCompatShadowedAttributes
public class AppCompatSpinner extends Spinner implements TintableBackgroundView {

    @SuppressLint("ResourceType")
    @StyleableRes
    private static final int[] ATTRS_ANDROID_SPINNERMODE = {android.R.attr.spinnerMode};

    private static final int MAX_ITEMS_MEASURED = 15;

    private static final String TAG = "AppCompatSpinner";

    private static final int MODE_DIALOG = 0;
    private static final int MODE_DROPDOWN = 1;
    private static final int MODE_THEME = -1;

    private final AppCompatBackgroundHelper mBackgroundTintHelper;

    /** Context used to inflate the popup window or dialog. */
    private final Context mPopupContext;

    /** Forwarding listener used to implement drag-to-open. */
    private ForwardingListener mForwardingListener;

    /** Temporary holder for setAdapter() calls from the super constructor. */
    private SpinnerAdapter mTempAdapter;

    private final boolean mPopupSet;

    private SpinnerPopup mPopup;

    int mDropDownWidth;

    final Rect mTempRect = new Rect();

    /**
     * Construct a new spinner with the given context's theme.
     *
     * @param context The Context the view is running in, through which it can
     *                access the current theme, resources, etc.
     */
    public AppCompatSpinner(
            @NonNull Context context) {
        this(context, null);
    }

    /**
     * Construct a new spinner with the given context's theme and the supplied
     * mode of displaying choices. <code>mode</code> may be one of
     * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN}.
     *
     * @param context The Context the view is running in, through which it can
     *                access the current theme, resources, etc.
     * @param mode    Constant describing how the user will select choices from the spinner.
     * @see #MODE_DIALOG
     * @see #MODE_DROPDOWN
     */
    public AppCompatSpinner(
            @NonNull Context context, int mode) {
        this(context, null, R.attr.spinnerStyle, mode);
    }

    /**
     * Construct a new spinner with the given context's theme and the supplied attribute set.
     *
     * @param context The Context the view is running in, through which it can
     *                access the current theme, resources, etc.
     * @param attrs   The attributes of the XML tag that is inflating the view.
     */
    public AppCompatSpinner(
            @NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, R.attr.spinnerStyle);
    }

    /**
     * Construct a new spinner with the given context's theme, the supplied attribute set,
     * and default style attribute.
     *
     * @param context      The Context the view is running in, through which it can
     *                     access the current theme, resources, etc.
     * @param attrs        The attributes of the XML tag that is inflating the view.
     * @param defStyleAttr An attribute in the current theme that contains a
     *                     reference to a style resource that supplies default values for
     *                     the view. Can be 0 to not look for defaults.
     */
    public AppCompatSpinner(
            @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, MODE_THEME);
    }

    /**
     * Construct a new spinner with the given context's theme, the supplied attribute set,
     * and default style. <code>mode</code> may be one of {@link #MODE_DIALOG} or
     * {@link #MODE_DROPDOWN} and determines how the user will select choices from the spinner.
     *
     * @param context      The Context the view is running in, through which it can
     *                     access the current theme, resources, etc.
     * @param attrs        The attributes of the XML tag that is inflating the view.
     * @param defStyleAttr An attribute in the current theme that contains a
     *                     reference to a style resource that supplies default values for
     *                     the view. Can be 0 to not look for defaults.
     * @param mode         Constant describing how the user will select choices from the spinner.
     * @see #MODE_DIALOG
     * @see #MODE_DROPDOWN
     */
    public AppCompatSpinner(
            @NonNull Context context, @Nullable  AttributeSet attrs, int defStyleAttr, int mode) {
        this(context, attrs, defStyleAttr, mode, null);
    }


    /**
     * Constructs a new spinner with the given context's theme, the supplied
     * attribute set, default styles, popup mode (one of {@link #MODE_DIALOG}
     * or {@link #MODE_DROPDOWN}), and the context against which the popup
     * should be inflated.
     *
     * @param context      The context against which the view is inflated, which
     *                     provides access to the current theme, resources, etc.
     * @param attrs        The attributes of the XML tag that is inflating the view.
     * @param defStyleAttr An attribute in the current theme that contains a
     *                     reference to a style resource that supplies default
     *                     values for the view. Can be 0 to not look for
     *                     defaults.
     * @param mode         Constant describing how the user will select choices from
     *                     the spinner.
     * @param popupTheme   The theme against which the dialog or dropdown popup
     *                     should be inflated. May be {@code null} to use the
     *                     view theme. If set, this will override any value
     *                     specified by
     *                     {@link R.styleable#Spinner_popupTheme}.
     * @see #MODE_DIALOG
     * @see #MODE_DROPDOWN
     */
    public AppCompatSpinner(@NonNull Context context, @Nullable AttributeSet attrs,
            int defStyleAttr, int mode, Resources.Theme popupTheme) {
        super(context, attrs, defStyleAttr);

        ThemeUtils.checkAppCompatTheme(this, getContext());

        TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
                R.styleable.Spinner, defStyleAttr, 0);

        mBackgroundTintHelper = new AppCompatBackgroundHelper(this);

        if (popupTheme != null) {
            mPopupContext = new ContextThemeWrapper(context, popupTheme);
        } else {
            final int popupThemeResId = a.getResourceId(R.styleable.Spinner_popupTheme, 0);
            if (popupThemeResId != 0) {
                mPopupContext = new ContextThemeWrapper(context, popupThemeResId);
            } else {
                mPopupContext = context;
            }
        }

        if (mode == MODE_THEME) {
            TypedArray aa = null;
            try {
                aa = context.obtainStyledAttributes(attrs, ATTRS_ANDROID_SPINNERMODE,
                        defStyleAttr, 0);
                if (aa.hasValue(0)) {
                    mode = aa.getInt(0, MODE_DIALOG);
                }
            } catch (Exception e) {
                Log.i(TAG, "Could not read android:spinnerMode", e);
            } finally {
                if (aa != null) {
                    aa.recycle();
                }
            }
        }

        switch (mode) {
            case MODE_DIALOG: {
                mPopup = new AppCompatSpinner.DialogPopup();
                mPopup.setPromptText(a.getString(R.styleable.Spinner_android_prompt));
                break;
            }
            case MODE_DROPDOWN: {
                final DropdownPopup popup = new DropdownPopup(mPopupContext, attrs, defStyleAttr);
                final TintTypedArray pa = TintTypedArray.obtainStyledAttributes(
                        mPopupContext, attrs, R.styleable.Spinner, defStyleAttr, 0);
                mDropDownWidth = pa.getLayoutDimension(R.styleable.Spinner_android_dropDownWidth,
                        LayoutParams.WRAP_CONTENT);
                popup.setBackgroundDrawable(
                        pa.getDrawable(R.styleable.Spinner_android_popupBackground));
                popup.setPromptText(a.getString(R.styleable.Spinner_android_prompt));
                pa.recycle();

                mPopup = popup;
                mForwardingListener = new ForwardingListener(this) {
                    @Override
                    public ShowableListMenu getPopup() {
                        return popup;
                    }

                    @Override
                    @SuppressLint("SyntheticAccessor")
                    public boolean onForwardingStarted() {
                        if (!getInternalPopup().isShowing()) {
                            showPopup();
                        }
                        return true;
                    }
                };
            }
        }

        final CharSequence[] entries = a.getTextArray(R.styleable.Spinner_android_entries);
        if (entries != null) {
            final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<>(
                    context, android.R.layout.simple_spinner_item, entries);
            adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item);
            setAdapter(adapter);
        }

        a.recycle();

        mPopupSet = true;

        // Base constructors can call setAdapter before we initialize mPopup.
        // Finish setting things up if this happened.
        if (mTempAdapter != null) {
            setAdapter(mTempAdapter);
            mTempAdapter = null;
        }

        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
    }

    /**
     * @return the context used to inflate the Spinner's popup or dialog window
     */
    @Override
    public Context getPopupContext() {
        return mPopupContext;
    }

    @Override
    public void setPopupBackgroundDrawable(Drawable background) {
        if (mPopup != null) {
            mPopup.setBackgroundDrawable(background);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            super.setPopupBackgroundDrawable(background);
        }
    }

    @Override
    public void setPopupBackgroundResource(@DrawableRes int resId) {
        setPopupBackgroundDrawable(AppCompatResources.getDrawable(getPopupContext(), resId));
    }

    @Override
    public Drawable getPopupBackground() {
        if (mPopup != null) {
            return mPopup.getBackground();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            return super.getPopupBackground();
        }
        return null;
    }

    @Override
    public void setDropDownVerticalOffset(int pixels) {
        if (mPopup != null) {
            mPopup.setVerticalOffset(pixels);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            super.setDropDownVerticalOffset(pixels);
        }
    }

    @Override
    public int getDropDownVerticalOffset() {
        if (mPopup != null) {
            return mPopup.getVerticalOffset();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            return super.getDropDownVerticalOffset();
        }
        return 0;
    }

    @Override
    public void setDropDownHorizontalOffset(int pixels) {
        if (mPopup != null) {
            mPopup.setHorizontalOriginalOffset(pixels);
            mPopup.setHorizontalOffset(pixels);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            super.setDropDownHorizontalOffset(pixels);
        }
    }

    /**
     * Get the configured horizontal offset in pixels for the spinner's popup window of choices.
     * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
     *
     * @return Horizontal offset in pixels
     */
    @Override
    public int getDropDownHorizontalOffset() {
        if (mPopup != null) {
            return mPopup.getHorizontalOffset();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            return super.getDropDownHorizontalOffset();
        }
        return 0;
    }

    @Override
    public void setDropDownWidth(int pixels) {
        if (mPopup != null) {
            mDropDownWidth = pixels;
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            super.setDropDownWidth(pixels);
        }
    }

    @Override
    public int getDropDownWidth() {
        if (mPopup != null) {
            return mDropDownWidth;
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            return super.getDropDownWidth();
        }
        return 0;
    }

    @Override
    public void setAdapter(SpinnerAdapter adapter) {
        // The super constructor may call setAdapter before we're prepared.
        // Postpone doing anything until we've finished construction.
        if (!mPopupSet) {
            mTempAdapter = adapter;
            return;
        }

        super.setAdapter(adapter);

        if (mPopup != null) {
            final Context popupContext = mPopupContext == null ? getContext() : mPopupContext;
            mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme()));
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        if (mPopup != null && mPopup.isShowing()) {
            mPopup.dismiss();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) {
            return true;
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
            final int measuredWidth = getMeasuredWidth();
            setMeasuredDimension(Math.min(Math.max(measuredWidth,
                                    compatMeasureContentWidth(getAdapter(), getBackground())),
                            MeasureSpec.getSize(widthMeasureSpec)),
                    getMeasuredHeight());
        }
    }

    @Override
    public boolean performClick() {
        if (mPopup != null) {
            // If we have a popup, show it if needed, or just consume the click...
            if (!mPopup.isShowing()) {
                showPopup();
            }
            return true;
        }

        // Else let the platform handle the click
        return super.performClick();
    }

    @Override
    public void setPrompt(CharSequence prompt) {
        if (mPopup != null) {
            mPopup.setPromptText(prompt);
        } else {
            super.setPrompt(prompt);
        }
    }

    @Override
    public CharSequence getPrompt() {
        return mPopup != null ? mPopup.getHintText() : super.getPrompt();
    }

    @Override
    public void setBackgroundResource(@DrawableRes int resId) {
        super.setBackgroundResource(resId);
        if (mBackgroundTintHelper != null) {
            mBackgroundTintHelper.onSetBackgroundResource(resId);
        }
    }

    @Override
    public void setBackgroundDrawable(@Nullable Drawable background) {
        super.setBackgroundDrawable(background);
        if (mBackgroundTintHelper != null) {
            mBackgroundTintHelper.onSetBackgroundDrawable(background);
        }
    }

    /**
     * This should be accessed via
     * {@link androidx.core.view.ViewCompat#setBackgroundTintList(android.view.View,
     * ColorStateList)}
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    @Override
    public void setSupportBackgroundTintList(@Nullable ColorStateList tint) {
        if (mBackgroundTintHelper != null) {
            mBackgroundTintHelper.setSupportBackgroundTintList(tint);
        }
    }

    /**
     * This should be accessed via
     * {@link androidx.core.view.ViewCompat#getBackgroundTintList(android.view.View)}
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    @Override
    @Nullable
    public ColorStateList getSupportBackgroundTintList() {
        return mBackgroundTintHelper != null
                ? mBackgroundTintHelper.getSupportBackgroundTintList() : null;
    }

    /**
     * This should be accessed via
     * {@link androidx.core.view.ViewCompat#setBackgroundTintMode(android.view.View,
     * PorterDuff.Mode)}
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    @Override
    public void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
        if (mBackgroundTintHelper != null) {
            mBackgroundTintHelper.setSupportBackgroundTintMode(tintMode);
        }
    }

    /**
     * This should be accessed via
     * {@link androidx.core.view.ViewCompat#getBackgroundTintMode(android.view.View)}
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    @Override
    @Nullable
    public PorterDuff.Mode getSupportBackgroundTintMode() {
        return mBackgroundTintHelper != null
                ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        if (mBackgroundTintHelper != null) {
            mBackgroundTintHelper.applySupportBackgroundTint();
        }
    }

    int compatMeasureContentWidth(SpinnerAdapter adapter, Drawable background) {
        if (adapter == null) {
            return 0;
        }

        int width = 0;
        View itemView = null;
        int itemType = 0;
        final int widthMeasureSpec =
                MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.UNSPECIFIED);
        final int heightMeasureSpec =
                MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED);

        // Make sure the number of items we'll measure is capped. If it's a huge data set
        // with wildly varying sizes, oh well.
        int start = Math.max(0, getSelectedItemPosition());
        final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
        final int count = end - start;
        start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
        for (int i = start; i < end; i++) {
            final int positionType = adapter.getItemViewType(i);
            if (positionType != itemType) {
                itemType = positionType;
                itemView = null;
            }
            itemView = adapter.getView(i, itemView, this);
            if (itemView.getLayoutParams() == null) {
                itemView.setLayoutParams(new LayoutParams(
                        LayoutParams.WRAP_CONTENT,
                        LayoutParams.WRAP_CONTENT));
            }
            itemView.measure(widthMeasureSpec, heightMeasureSpec);
            width = Math.max(width, itemView.getMeasuredWidth());
        }

        // Add background padding to measured width
        if (background != null) {
            background.getPadding(mTempRect);
            width += mTempRect.left + mTempRect.right;
        }

        return width;
    }

    @VisibleForTesting
    final SpinnerPopup getInternalPopup() {
        return mPopup;
    }

    void showPopup() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            mPopup.show(Api17Impl.getTextDirection(this), Api17Impl.getTextAlignment(this));
        } else {
            mPopup.show(-1, -1);
        }
    }


    @Override
    public Parcelable onSaveInstanceState() {
        final AppCompatSpinner.SavedState ss =
                new AppCompatSpinner.SavedState(super.onSaveInstanceState());
        ss.mShowDropdown = mPopup != null && mPopup.isShowing();
        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        AppCompatSpinner.SavedState ss = (AppCompatSpinner.SavedState) state;

        super.onRestoreInstanceState(ss.getSuperState());

        if (ss.mShowDropdown) {
            ViewTreeObserver vto = getViewTreeObserver();
            if (vto != null) {
                final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        if (!getInternalPopup().isShowing()) {
                            showPopup();
                        }
                        final ViewTreeObserver vto = getViewTreeObserver();
                        if (vto != null) {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                                Api16Impl.removeOnGlobalLayoutListener(vto, this);
                            } else {
                                vto.removeGlobalOnLayoutListener(this);
                            }
                        }
                    }
                };
                vto.addOnGlobalLayoutListener(listener);
            }
        }
    }

    static class SavedState extends BaseSavedState {
        boolean mShowDropdown;

        SavedState(Parcelable superState) {
            super(superState);
        }

        SavedState(Parcel in) {
            super(in);
            mShowDropdown = in.readByte() != 0;
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeByte((byte) (mShowDropdown ? 1 : 0));
        }

        public static final Parcelable.Creator<SavedState> CREATOR =
                new Parcelable.Creator<SavedState>() {
                    @Override
                    public SavedState createFromParcel(Parcel in) {
                        return new SavedState(in);
                    }

                    @Override
                    public SavedState[] newArray(int size) {
                        return new SavedState[size];
                    }
                };
    }

    /**
     * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
     * into a ListAdapter.</p>
     */
    private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {

        private SpinnerAdapter mAdapter;

        private ListAdapter mListAdapter;

        /**
         * Creates a new ListAdapter wrapper for the specified adapter.
         *
         * @param adapter       the SpinnerAdapter to transform into a ListAdapter
         * @param dropDownTheme the theme against which to inflate drop-down
         *                      views, may be {@null} to use default theme
         */
        public DropDownAdapter(@Nullable SpinnerAdapter adapter,
                @Nullable Resources.Theme dropDownTheme) {
            mAdapter = adapter;

            if (adapter instanceof ListAdapter) {
                mListAdapter = (ListAdapter) adapter;
            }

            if (dropDownTheme != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                         && adapter instanceof android.widget.ThemedSpinnerAdapter) {
                    final android.widget.ThemedSpinnerAdapter themedAdapter =
                            (android.widget.ThemedSpinnerAdapter) adapter;
                    Api23Impl.setDropDownViewTheme(themedAdapter, dropDownTheme);
                } else if (adapter instanceof ThemedSpinnerAdapter) {
                    final ThemedSpinnerAdapter themedAdapter = (ThemedSpinnerAdapter) adapter;
                    if (themedAdapter.getDropDownViewTheme() == null) {
                        themedAdapter.setDropDownViewTheme(dropDownTheme);
                    }
                }
            }
        }

        @Override
        public int getCount() {
            return mAdapter == null ? 0 : mAdapter.getCount();
        }

        @Override
        public Object getItem(int position) {
            return mAdapter == null ? null : mAdapter.getItem(position);
        }

        @Override
        public long getItemId(int position) {
            return mAdapter == null ? -1 : mAdapter.getItemId(position);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return getDropDownView(position, convertView, parent);
        }

        @Override
        public View getDropDownView(int position, View convertView, ViewGroup parent) {
            return (mAdapter == null) ? null
                    : mAdapter.getDropDownView(position, convertView, parent);
        }

        @Override
        public boolean hasStableIds() {
            return mAdapter != null && mAdapter.hasStableIds();
        }

        @Override
        public void registerDataSetObserver(DataSetObserver observer) {
            if (mAdapter != null) {
                mAdapter.registerDataSetObserver(observer);
            }
        }

        @Override
        public void unregisterDataSetObserver(DataSetObserver observer) {
            if (mAdapter != null) {
                mAdapter.unregisterDataSetObserver(observer);
            }
        }

        /**
         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
         * Otherwise, return true.
         */
        @Override
        public boolean areAllItemsEnabled() {
            final ListAdapter adapter = mListAdapter;
            if (adapter != null) {
                return adapter.areAllItemsEnabled();
            } else {
                return true;
            }
        }

        /**
         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
         * Otherwise, return true.
         */
        @Override
        public boolean isEnabled(int position) {
            final ListAdapter adapter = mListAdapter;
            if (adapter != null) {
                return adapter.isEnabled(position);
            } else {
                return true;
            }
        }

        @Override
        public int getItemViewType(int position) {
            return 0;
        }

        @Override
        public int getViewTypeCount() {
            return 1;
        }

        @Override
        public boolean isEmpty() {
            return getCount() == 0;
        }
    }

    /**
     * Implements some sort of popup selection interface for selecting a spinner option.
     * Allows for different spinner modes.
     */
    @VisibleForTesting
    interface SpinnerPopup {
        void setAdapter(ListAdapter adapter);

        /**
         * Show the popup
         */
        void show(int textDirection, int textAlignment);

        /**
         * Dismiss the popup
         */
        void dismiss();

        /**
         * @return true if the popup is showing, false otherwise.
         */
        boolean isShowing();

        /**
         * Set hint text to be displayed to the user. This should provide
         * a description of the choice being made.
         * @param hintText Hint text to set.
         */
        void setPromptText(CharSequence hintText);
        CharSequence getHintText();

        void setBackgroundDrawable(Drawable bg);
        void setVerticalOffset(int px);
        void setHorizontalOffset(int px);
        void setHorizontalOriginalOffset(int px);
        int getHorizontalOriginalOffset();
        Drawable getBackground();
        int getVerticalOffset();
        int getHorizontalOffset();
    }

    @VisibleForTesting
    class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
        @VisibleForTesting
        AlertDialog mPopup;
        private ListAdapter mListAdapter;
        private CharSequence mPrompt;

        @Override
        public void dismiss() {
            if (mPopup != null) {
                mPopup.dismiss();
                mPopup = null;
            }
        }

        @Override
        public boolean isShowing() {
            return mPopup != null ? mPopup.isShowing() : false;
        }

        @Override
        public void setAdapter(ListAdapter adapter) {
            mListAdapter = adapter;
        }

        @Override
        public void setPromptText(CharSequence hintText) {
            mPrompt = hintText;
        }

        @Override
        public CharSequence getHintText() {
            return mPrompt;
        }

        @Override
        public void show(int textDirection, int textAlignment) {
            if (mListAdapter == null) {
                return;
            }
            AlertDialog.Builder builder = new AlertDialog.Builder(getPopupContext());
            if (mPrompt != null) {
                builder.setTitle(mPrompt);
            }
            mPopup = builder.setSingleChoiceItems(mListAdapter,
                    getSelectedItemPosition(), this).create();
            final ListView listView = mPopup.getListView();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                Api17Impl.setTextDirection(listView, textDirection);
                Api17Impl.setTextAlignment(listView, textAlignment);
            }
            mPopup.show();
        }

        @Override
        public void onClick(DialogInterface dialog, int which) {
            setSelection(which);
            if (getOnItemClickListener() != null) {
                performItemClick(null, which, mListAdapter.getItemId(which));
            }
            dismiss();
        }

        @Override
        public void setBackgroundDrawable(Drawable bg) {
            Log.e(TAG, "Cannot set popup background for MODE_DIALOG, ignoring");
        }

        @Override
        public void setVerticalOffset(int px) {
            Log.e(TAG, "Cannot set vertical offset for MODE_DIALOG, ignoring");
        }

        @Override
        public void setHorizontalOffset(int px) {
            Log.e(TAG, "Cannot set horizontal offset for MODE_DIALOG, ignoring");
        }

        @Override
        public Drawable getBackground() {
            return null;
        }

        @Override
        public int getVerticalOffset() {
            return 0;
        }

        @Override
        public int getHorizontalOffset() {
            return 0;
        }

        @Override
        public void setHorizontalOriginalOffset(int px) {
            Log.e(TAG, "Cannot set horizontal (original) offset for MODE_DIALOG, ignoring");
        }

        @Override
        public int getHorizontalOriginalOffset() {
            return 0;
        }
    }

    @VisibleForTesting
    class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
        private CharSequence mHintText;
        ListAdapter mAdapter;
        private final Rect mVisibleRect = new Rect();
        private int mOriginalHorizontalOffset;

        public DropdownPopup(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);

            setAnchorView(AppCompatSpinner.this);
            setModal(true);
            setPromptPosition(POSITION_PROMPT_ABOVE);

            setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
                    AppCompatSpinner.this.setSelection(position);
                    if (getOnItemClickListener() != null) {
                        AppCompatSpinner.this
                                .performItemClick(v, position, mAdapter.getItemId(position));
                    }
                    dismiss();
                }
            });
        }

        @Override
        public void setAdapter(ListAdapter adapter) {
            super.setAdapter(adapter);
            mAdapter = adapter;
        }

        @Override
        public CharSequence getHintText() {
            return mHintText;
        }

        @Override
        public void setPromptText(CharSequence hintText) {
            // Hint text is ignored for dropdowns, but maintain it here.
            mHintText = hintText;
        }

        void computeContentWidth() {
            final Drawable background = getBackground();
            int hOffset = 0;
            if (background != null) {
                background.getPadding(mTempRect);
                hOffset = ViewUtils.isLayoutRtl(AppCompatSpinner.this) ? mTempRect.right
                        : -mTempRect.left;
            } else {
                mTempRect.left = mTempRect.right = 0;
            }

            final int spinnerPaddingLeft = AppCompatSpinner.this.getPaddingLeft();
            final int spinnerPaddingRight = AppCompatSpinner.this.getPaddingRight();
            final int spinnerWidth = AppCompatSpinner.this.getWidth();
            if (mDropDownWidth == WRAP_CONTENT) {
                int contentWidth = compatMeasureContentWidth(
                        (SpinnerAdapter) mAdapter, getBackground());
                final int contentWidthLimit = getContext().getResources()
                        .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
                if (contentWidth > contentWidthLimit) {
                    contentWidth = contentWidthLimit;
                }
                setContentWidth(Math.max(
                        contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
            } else if (mDropDownWidth == MATCH_PARENT) {
                setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
            } else {
                setContentWidth(mDropDownWidth);
            }
            if (ViewUtils.isLayoutRtl(AppCompatSpinner.this)) {
                hOffset += spinnerWidth - spinnerPaddingRight - getWidth()
                        - getHorizontalOriginalOffset();
            } else {
                hOffset += spinnerPaddingLeft + getHorizontalOriginalOffset();
            }
            setHorizontalOffset(hOffset);
        }

        @Override
        public void show(int textDirection, int textAlignment) {
            final boolean wasShowing = isShowing();

            computeContentWidth();

            setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
            super.show();
            final ListView listView = getListView();
            listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                Api17Impl.setTextDirection(listView, textDirection);
                Api17Impl.setTextAlignment(listView, textAlignment);
            }
            setSelection(AppCompatSpinner.this.getSelectedItemPosition());

            if (wasShowing) {
                // Skip setting up the layout/dismiss listener below. If we were previously
                // showing it will still stick around.
                return;
            }

            // Make sure we hide if our anchor goes away.
            // TODO: This might be appropriate to push all the way down to PopupWindow,
            // but it may have other side effects to investigate first. (Text editing handles, etc.)
            final ViewTreeObserver vto = getViewTreeObserver();
            if (vto != null) {
                final ViewTreeObserver.OnGlobalLayoutListener layoutListener
                        = new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        if (!isVisibleToUser(AppCompatSpinner.this)) {
                            dismiss();
                        } else {
                            computeContentWidth();

                            // Use super.show here to update; we don't want to move the selected
                            // position or adjust other things that would be reset otherwise.
                            DropdownPopup.super.show();
                        }
                    }
                };
                vto.addOnGlobalLayoutListener(layoutListener);
                setOnDismissListener(new PopupWindow.OnDismissListener() {
                    @Override
                    public void onDismiss() {
                        final ViewTreeObserver vto = getViewTreeObserver();
                        if (vto != null) {
                            vto.removeGlobalOnLayoutListener(layoutListener);
                        }
                    }
                });
            }
        }

        /**
         * Simplified version of the the hidden View.isVisibleToUser()
         */
        boolean isVisibleToUser(View view) {
            return ViewCompat.isAttachedToWindow(view) && view.getGlobalVisibleRect(mVisibleRect);
        }

        @Override
        public void setHorizontalOriginalOffset(int px) {
            mOriginalHorizontalOffset = px;
        }

        @Override
        public int getHorizontalOriginalOffset() {
            return mOriginalHorizontalOffset;
        }
    }

    @RequiresApi(23)
    private static final class Api23Impl {
        private Api23Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static void setDropDownViewTheme(
                @NonNull android.widget.ThemedSpinnerAdapter themedSpinnerAdapter,
                @Nullable Resources.Theme theme
        ) {
            if (!ObjectsCompat.equals(themedSpinnerAdapter.getDropDownViewTheme(), theme)) {
                themedSpinnerAdapter.setDropDownViewTheme(theme);
            }
        }
    }

    @RequiresApi(17)
    private static final class Api17Impl {
        private Api17Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static int getTextAlignment(@NonNull View view) {
            return view.getTextAlignment();
        }

        @DoNotInline
        static void setTextAlignment(@NonNull View view, int textAlignment) {
            view.setTextAlignment(textAlignment);
        }

        @DoNotInline
        static int getTextDirection(@NonNull View view) {
            return view.getTextDirection();
        }

        @DoNotInline
        static void setTextDirection(@NonNull View view, int textDirection) {
            view.setTextDirection(textDirection);
        }
    }

    @RequiresApi(16)
    private static final class Api16Impl {
        private Api16Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static void removeOnGlobalLayoutListener(
                @NonNull ViewTreeObserver viewTreeObserver,
                @Nullable OnGlobalLayoutListener victim
        ) {
            viewTreeObserver.removeOnGlobalLayoutListener(victim);
        }
    }
}