public class

AnimatedStateListDrawableCompat

extends androidx.appcompat.graphics.drawable.StateListDrawable

implements TintAwareDrawable

 java.lang.Object

↳Drawable

↳androidx.appcompat.graphics.drawable.DrawableContainer

↳androidx.appcompat.graphics.drawable.StateListDrawable

↳androidx.appcompat.graphics.drawable.AnimatedStateListDrawableCompat

Gradle dependencies

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

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

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

Androidx class mapping:

androidx.appcompat.graphics.drawable.AnimatedStateListDrawableCompat android.support.v7.graphics.drawable.AnimatedStateListDrawableCompat

Overview

A Drawable providing animated transitions between states.

A port of compatible with older versions of the platform.

This drawable can be defined in an XML file with the <animated-selector> element. Each keyframe Drawable is defined in a nested <item> element. Transitions are defined in a nested <transition> element.

Notable exceptions not supported by this class:

  • drawables defined as children of <item>s or <transition>s (except vectors) ignore theme attributes prior to API level 21
  • Animated vector transitions do not support reversing prior to API level 24

Summary

Constructors
publicAnimatedStateListDrawableCompat()

Methods
public voidaddState(int[] stateSet[], Drawable drawable, int id)

Add a new drawable to the set of keyframes.

public voidaddTransition(int fromId, int toId, Drawable transition, boolean reversible)

Adds a new transition between keyframes.

public static AnimatedStateListDrawableCompatcreate(Context context, int resId, Resources.Theme theme)

Creates an AnimatedStateListDrawableCompat.

public static AnimatedStateListDrawableCompatcreateFromXmlInner(Context context, Resources resources, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)

Create a AnimatedStateListDrawableCompat from inside an XML document using an optional .

public voidinflate(Context context, Resources resources, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)

Inflate this Drawable from an XML resource optionally styled by a theme.

public booleanisStateful()

public voidjumpToCurrentState()

public Drawablemutate()

protected booleanonStateChange(int[] stateSet[])

public booleansetVisible(boolean visible, boolean restart)

from androidx.appcompat.graphics.drawable.StateListDrawableaddState, applyTheme, inflate
from androidx.appcompat.graphics.drawable.DrawableContainercanApplyTheme, draw, getAlpha, getChangingConfigurations, getConstantState, getCurrent, getHotspotBounds, getIntrinsicHeight, getIntrinsicWidth, getMinimumHeight, getMinimumWidth, getOpacity, getOutline, getPadding, invalidateDrawable, isAutoMirrored, onBoundsChange, onLayoutDirectionChanged, onLevelChange, scheduleDrawable, setAlpha, setAutoMirrored, setColorFilter, setDither, setEnterFadeDuration, setExitFadeDuration, setHotspot, setHotspotBounds, setTintList, setTintMode, unscheduleDrawable
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public AnimatedStateListDrawableCompat()

Methods

public static AnimatedStateListDrawableCompat create(Context context, int resId, Resources.Theme theme)

Creates an AnimatedStateListDrawableCompat.

Parameters:

context: context to inflate against
resId: the resource ID for AnimatedStateListDrawable object.
theme: the theme to apply, may be null.

Returns:

a new AnimatedStateListDrawableCompat or null if parsing error is found.

public static AnimatedStateListDrawableCompat createFromXmlInner(Context context, Resources resources, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)

Create a AnimatedStateListDrawableCompat from inside an XML document using an optional . Called on a parser positioned at a tag in an XML document, tries to create an AnimatedStateListDrawableCompat from that tag.

public void inflate(Context context, Resources resources, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)

Inflate this Drawable from an XML resource optionally styled by a theme. This can't be called more than once for each Drawable.

Parameters:

context: context to inflate against
resources: Resources used to resolve attribute values
parser: XML parser from which to inflate this Drawable
attrs: Base set of attribute values
theme: Theme to apply, may be null

public boolean setVisible(boolean visible, boolean restart)

public void addState(int[] stateSet[], Drawable drawable, int id)

Add a new drawable to the set of keyframes.

Parameters:

stateSet: An array of resource IDs to associate with the keyframe
drawable: The drawable to show when in the specified state, may not be null
id: The unique identifier for the keyframe

public void addTransition(int fromId, int toId, Drawable transition, boolean reversible)

Adds a new transition between keyframes.

Parameters:

fromId: Unique identifier of the starting keyframe
toId: Unique identifier of the ending keyframe
transition: An drawable to use as a transition, may not be null
reversible: Whether the transition can be reversed

public boolean isStateful()

public void jumpToCurrentState()

protected boolean onStateChange(int[] stateSet[])

public Drawable mutate()

Source

/*
 * Copyright (C) 2018 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.graphics.drawable;

import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;

import static androidx.core.content.res.TypedArrayUtils.obtainAttributes;

import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.StateSet;
import android.util.Xml;

import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.resources.Compatibility;
import androidx.appcompat.resources.R;
import androidx.appcompat.widget.ResourceManagerInternal;
import androidx.collection.LongSparseArray;
import androidx.collection.SparseArrayCompat;
import androidx.core.graphics.drawable.TintAwareDrawable;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;

/**
 * A {@link Drawable} providing animated transitions between states.
 *
 * <p>A port of {@link android.graphics.drawable.AnimatedStateListDrawable} compatible with older
 * versions of the platform.
 *
 * <p>This drawable can be defined in an XML file with the <code>
 * &lt;animated-selector></code> element. Each keyframe Drawable is defined in a
 * nested <code>&lt;item></code> element. Transitions are defined in a nested
 * <code>&lt;transition></code> element.
 *
 * <p>Notable exceptions not supported by this class:
 *
 * <ul>
 * <li><code>drawable</code>s defined as children of <code>&lt;item></code>s or
 * <code>&lt;transition></code>s (<b>except</b> vectors) ignore theme attributes prior to API level
 * 21</li>
 * <li>Animated vector transitions do not support reversing prior to API level 24</li>
 * </ul>
 *
 * {@link android.R.attr#state_focused}
 * {@link android.R.attr#state_window_focused}
 * {@link android.R.attr#state_enabled}
 * {@link android.R.attr#state_checkable}
 * {@link android.R.attr#state_checked}
 * {@link android.R.attr#state_selected}
 * {@link android.R.attr#state_activated}
 * {@link android.R.attr#state_active}
 * {@link android.R.attr#state_single}
 * {@link android.R.attr#state_first}
 * {@link android.R.attr#state_middle}
 * {@link android.R.attr#state_last}
 * {@link android.R.attr#state_pressed}
 */
@SuppressLint("RestrictedAPI") // Temporary until we have correct restriction scopes for 1.0
public class AnimatedStateListDrawableCompat extends StateListDrawable
        implements TintAwareDrawable {
    private static final String LOGTAG = AnimatedStateListDrawableCompat.class.getSimpleName();
    private static final String ELEMENT_TRANSITION = "transition";
    private static final String ELEMENT_ITEM = "item";
    private static final String TRANSITION_MISSING_DRAWABLE_ERROR =
            ": <transition> tag requires a 'drawable' attribute or child tag defining a drawable";
    private static final String TRANSITION_MISSING_FROM_TO_ID =
            ": <transition> tag requires 'fromId' & 'toId' attributes";
    private static final String ITEM_MISSING_DRAWABLE_ERROR =
            ": <item> tag requires a 'drawable' attribute or child tag defining a drawable";
    private AnimatedStateListState mState;
    /** The currently running transition, if any. */
    private Transition mTransition;
    /** Index to be set after the transition ends. */
    private int mTransitionToIndex = -1;
    /** Index away from which we are transitioning. */
    private int mTransitionFromIndex = -1;
    private boolean mMutated;

    public AnimatedStateListDrawableCompat() {
        this(null, null);
    }

    AnimatedStateListDrawableCompat(@Nullable AnimatedStateListState state,
            @Nullable Resources res) {
        super(null);
        // Every animated state list drawable has its own constant state.
        final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
        setConstantState(newState);
        onStateChange(getState());
        jumpToCurrentState();
    }

    /**
     * Creates an AnimatedStateListDrawableCompat.
     *
     * @param context context to inflate against
     * @param resId the resource ID for AnimatedStateListDrawable object.
     * @param theme the theme to apply, may be null.
     * @return a new AnimatedStateListDrawableCompat or null if parsing error is found.
     */
    @Nullable
    public static AnimatedStateListDrawableCompat create(
            @NonNull Context context,
            @DrawableRes int resId,
            @Nullable Resources.Theme theme) {
        try {
            final Resources res = context.getResources();
            @SuppressLint("ResourceType")
            final XmlPullParser parser = res.getXml(resId);
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                // Empty loop
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }
            return createFromXmlInner(context, res, parser, attrs, theme);
        } catch (XmlPullParserException e) {
            Log.e(LOGTAG, "parser error", e);
        } catch (IOException e) {
            Log.e(LOGTAG, "parser error", e);
        }
        return null;
    }

    /**
     * Create a AnimatedStateListDrawableCompat from inside an XML document using an optional
     * {@link Resources.Theme}. Called on a parser positioned at a tag in an XML
     * document, tries to create an AnimatedStateListDrawableCompat from that tag.
     */
    public static AnimatedStateListDrawableCompat createFromXmlInner(
            @NonNull Context context,
            @NonNull Resources resources,
            @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs,
            @Nullable Resources.Theme theme)
            throws IOException, XmlPullParserException {
        final String name = parser.getName();
        if (!name.equals("animated-selector")) {
            throw new XmlPullParserException(
                    parser.getPositionDescription() + ": invalid animated-selector tag " + name);
        }
        AnimatedStateListDrawableCompat asl = new AnimatedStateListDrawableCompat();
        asl.inflate(context, resources, parser, attrs, theme);
        return asl;
    }

    /**
     * Inflate this Drawable from an XML resource optionally styled by a theme.
     * This can't be called more than once for each Drawable.
     *
     * @param context context to inflate against
     * @param resources Resources used to resolve attribute values
     * @param parser XML parser from which to inflate this Drawable
     * @param attrs Base set of attribute values
     * @param theme Theme to apply, may be null
     * @throws XmlPullParserException
     * @throws IOException
     */
    @Override
    public void inflate(
            @NonNull Context context,
            @NonNull Resources resources,
            @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs,
            @Nullable Resources.Theme theme)
            throws XmlPullParserException, IOException {
        final TypedArray a = obtainAttributes(
                resources, theme, attrs, R.styleable.AnimatedStateListDrawableCompat);
        setVisible(a.getBoolean(R.styleable.AnimatedStateListDrawableCompat_android_visible, true),
                true);
        updateStateFromTypedArray(a);
        updateDensity(resources);
        a.recycle();
        inflateChildElements(context, resources, parser, attrs, theme);
        init();
    }

    @Override
    public boolean setVisible(boolean visible, boolean restart) {
        final boolean changed = super.setVisible(visible, restart);
        if (mTransition != null && (changed || restart)) {
            if (visible) {
                mTransition.start();
            } else {
                // Ensure we're showing the correct state when visible.
                jumpToCurrentState();
            }
        }
        return changed;
    }

    /**
     * Add a new drawable to the set of keyframes.
     *
     * @param stateSet An array of resource IDs to associate with the keyframe
     * @param drawable The drawable to show when in the specified state, may not be null
     * @param id       The unique identifier for the keyframe
     */
    public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
        if (drawable == null) {
            throw new IllegalArgumentException("Drawable must not be null");
        }
        mState.addStateSet(stateSet, drawable, id);
        onStateChange(getState());
    }

    /**
     * Adds a new transition between keyframes.
     *
     * @param fromId     Unique identifier of the starting keyframe
     * @param toId       Unique identifier of the ending keyframe
     * @param transition An {@link Animatable} drawable to use as a transition, may not be null
     * @param reversible Whether the transition can be reversed
     */
    public <T extends Drawable & Animatable> void addTransition(int fromId, int toId,
            @NonNull T transition, boolean reversible) {
        if (transition == null) {
            throw new IllegalArgumentException("Transition drawable must not be null");
        }
        mState.addTransition(fromId, toId, transition, reversible);
    }

    @Override
    public boolean isStateful() {
        return true;
    }

    @Override
    public void jumpToCurrentState() {
        super.jumpToCurrentState();
        if (mTransition != null) {
            mTransition.stop();
            mTransition = null;
            selectDrawable(mTransitionToIndex);
            mTransitionToIndex = -1;
            mTransitionFromIndex = -1;
        }
    }

    @Override
    protected boolean onStateChange(int[] stateSet) {
        // If we're not already at the target index, either attempt to find a
        // valid transition to it or jump directly there.
        final int targetIndex = mState.indexOfKeyframe(stateSet);
        boolean changed = targetIndex != getCurrentIndex()
                && (selectTransition(targetIndex) || selectDrawable(targetIndex));
        // We need to propagate the state change to the current drawable, but
        // we can't call StateListDrawable.onStateChange() without changing the
        // current drawable.
        final Drawable current = getCurrent();
        if (current != null) {
            changed |= current.setState(stateSet);
        }
        return changed;
    }

    private boolean selectTransition(int toIndex) {
        final int fromIndex;
        final Transition currentTransition = mTransition;
        if (currentTransition != null) {
            if (toIndex == mTransitionToIndex) {
                // Already animating to that keyframe.
                return true;
            } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) {
                // Reverse the current animation.
                currentTransition.reverse();
                mTransitionToIndex = mTransitionFromIndex;
                mTransitionFromIndex = toIndex;
                return true;
            }
            // Start the next transition from the end of the current one.
            fromIndex = mTransitionToIndex;
            // Changing animation, end the current animation.
            currentTransition.stop();
        } else {
            fromIndex = getCurrentIndex();
        }
        // Reset state.
        mTransition = null;
        mTransitionFromIndex = -1;
        mTransitionToIndex = -1;
        final AnimatedStateListState state = mState;
        final int fromId = state.getKeyframeIdAt(fromIndex);
        final int toId = state.getKeyframeIdAt(toIndex);
        if (toId == 0 || fromId == 0) {
            // Missing a keyframe ID.
            return false;
        }
        final int transitionIndex = state.indexOfTransition(fromId, toId);
        if (transitionIndex < 0) {
            // Couldn't select a transition.
            return false;
        }
        boolean hasReversibleFlag = state.transitionHasReversibleFlag(fromId, toId);
        // This may fail if we're already on the transition, but that's okay!
        selectDrawable(transitionIndex);
        final Transition transition;
        final Drawable d = getCurrent();
        if (d instanceof AnimationDrawable) {
            final boolean reversed = state.isTransitionReversed(fromId, toId);
            transition = new AnimationDrawableTransition((AnimationDrawable) d,
                    reversed, hasReversibleFlag);
        } else if (d instanceof AnimatedVectorDrawableCompat) {
            //final boolean reversed = state.isTransitionReversed(fromId, toId);
            transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawableCompat) d);
        } else if (d instanceof Animatable) {
            transition = new AnimatableTransition((Animatable) d);
        } else {
            // We don't know how to animate this transition.
            return false;
        }
        transition.start();
        mTransition = transition;
        mTransitionFromIndex = fromIndex;
        mTransitionToIndex = toIndex;
        return true;
    }

    private abstract static class Transition {
        public abstract void start();

        public abstract void stop();

        public void reverse() {
            // Not supported by default.
        }

        public boolean canReverse() {
            return false;
        }
    }

    private static class AnimatableTransition extends Transition {
        private final Animatable mA;

        AnimatableTransition(Animatable a) {
            mA = a;
        }

        @Override
        public void start() {
            mA.start();
        }

        @Override
        public void stop() {
            mA.stop();
        }
    }

    private static class AnimationDrawableTransition extends Transition {
        private final ObjectAnimator mAnim;
        // Even AnimationDrawable is always reversible technically, but
        // we should obey the XML's android:reversible flag.
        private final boolean mHasReversibleFlag;

        AnimationDrawableTransition(AnimationDrawable ad,
                boolean reversed, boolean hasReversibleFlag) {
            final int frameCount = ad.getNumberOfFrames();
            final int fromFrame = reversed ? frameCount - 1 : 0;
            final int toFrame = reversed ? 0 : frameCount - 1;
            final FrameInterpolator interp = new FrameInterpolator(ad, reversed);
            @SuppressLint("ObjectAnimatorBinding")
            final ObjectAnimator anim =
                    ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame);
            if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                Compatibility.Api18Impl.setAutoCancel(anim, true);
            }
            anim.setDuration(interp.getTotalDuration());
            anim.setInterpolator(interp);
            mHasReversibleFlag = hasReversibleFlag;
            mAnim = anim;
        }

        @Override
        public boolean canReverse() {
            return mHasReversibleFlag;
        }

        @Override
        public void start() {
            mAnim.start();
        }

        @Override
        public void reverse() {
            mAnim.reverse();
        }

        @Override
        public void stop() {
            mAnim.cancel();
        }
    }

    private static class AnimatedVectorDrawableTransition extends Transition {
        private final AnimatedVectorDrawableCompat mAvd;

        AnimatedVectorDrawableTransition(AnimatedVectorDrawableCompat avd) {
            mAvd = avd;
        }

        @Override
        public void start() {
            mAvd.start();
        }

        @Override
        public void stop() {
            mAvd.stop();
        }
    }

    private void updateStateFromTypedArray(TypedArray a) {
        final AnimatedStateListState state = mState;
        // Account for any configuration changes.
        if (SDK_INT >= LOLLIPOP) {
            state.mChangingConfigurations |= Compatibility.Api21Impl.getChangingConfigurations(a);
        }
        // Extract the theme attributes, if any.
        state.setVariablePadding(
                a.getBoolean(R.styleable.AnimatedStateListDrawableCompat_android_variablePadding,
                        state.mVariablePadding));
        state.setConstantSize(
                a.getBoolean(R.styleable.AnimatedStateListDrawableCompat_android_constantSize,
                        state.mConstantSize));
        state.setEnterFadeDuration(
                a.getInt(R.styleable.AnimatedStateListDrawableCompat_android_enterFadeDuration,
                        state.mEnterFadeDuration));
        state.setExitFadeDuration(
                a.getInt(R.styleable.AnimatedStateListDrawableCompat_android_exitFadeDuration,
                        state.mExitFadeDuration));
        setDither(a.getBoolean(R.styleable.AnimatedStateListDrawableCompat_android_dither,
                state.mDither));
    }

    private void init() {
        onStateChange(getState());
    }

    private void inflateChildElements(
            @NonNull Context context,
            @NonNull Resources resources,
            @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs,
            @Nullable Resources.Theme theme)
            throws XmlPullParserException, IOException {
        int type;
        final int innerDepth = parser.getDepth() + 1;
        int depth;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth
                || type != XmlPullParser.END_TAG)) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            if (depth > innerDepth) {
                continue;
            }
            if (parser.getName().equals(ELEMENT_ITEM)) {
                parseItem(context, resources, parser, attrs, theme);
            } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
                parseTransition(context, resources, parser, attrs, theme);
            }
        }
    }

    private int parseTransition(
            @NonNull Context context,
            @NonNull Resources resources,
            @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs,
            @Nullable Resources.Theme theme)
            throws XmlPullParserException, IOException {

        final TypedArray a = obtainAttributes(resources, theme, attrs,
                R.styleable.AnimatedStateListDrawableTransition);
        final int fromId = a.getResourceId(
                R.styleable.AnimatedStateListDrawableTransition_android_fromId, -1);
        final int toId = a.getResourceId(
                R.styleable.AnimatedStateListDrawableTransition_android_toId, -1);
        Drawable dr = null;
        final int drawableId = a.getResourceId(
                R.styleable.AnimatedStateListDrawableTransition_android_drawable, -1);
        if (drawableId > 0) {
            dr = ResourceManagerInternal.get().getDrawable(context, drawableId);
        }
        final boolean reversible = a.getBoolean(
                R.styleable.AnimatedStateListDrawableTransition_android_reversible, false);
        a.recycle();
        // Loading child elements modifies the state of the AttributeSet's underlying parser, so
        // it needs to happen after obtaining attributes and extracting states.
        if (dr == null) {
            int type;
            while ((type = parser.next()) == XmlPullParser.TEXT) {
                // no-op
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException(
                        parser.getPositionDescription() + TRANSITION_MISSING_DRAWABLE_ERROR);
            }
            // Attempt to parse child AVDs
            if (parser.getName().equals("animated-vector")) {
                dr = AnimatedVectorDrawableCompat.createFromXmlInner(context, resources, parser,
                        attrs, theme);
            } else if (SDK_INT >= LOLLIPOP) {
                dr = Compatibility.Api21Impl.createFromXmlInner(resources, parser, attrs, theme);
            } else {
                dr = Drawable.createFromXmlInner(resources, parser, attrs);
            }
        }
        if (dr == null) {
            throw new XmlPullParserException(
                    parser.getPositionDescription() + TRANSITION_MISSING_DRAWABLE_ERROR);
        }
        if (fromId == -1 || toId == -1) {
            throw new XmlPullParserException(
                    parser.getPositionDescription() + TRANSITION_MISSING_FROM_TO_ID);
        }
        return mState.addTransition(fromId, toId, dr, reversible);
    }

    private int parseItem(
            @NonNull Context context,
            @NonNull Resources resources,
            @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs,
            @Nullable Resources.Theme theme)
            throws XmlPullParserException, IOException {
        final TypedArray a = obtainAttributes(resources, theme, attrs,
                R.styleable.AnimatedStateListDrawableItem);
        final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_android_id,
                0);
        Drawable dr = null;
        final int drawableId =
                a.getResourceId(R.styleable.AnimatedStateListDrawableItem_android_drawable, -1);
        if (drawableId > 0) {
            dr = ResourceManagerInternal.get().getDrawable(context, drawableId);
        }
        a.recycle();
        final int[] states = extractStateSet(attrs);
        // Loading child elements modifies the state of the AttributeSet's underlying parser, so
        // it needs to happen after obtaining attributes and extracting states.
        if (dr == null) {
            int type;
            while ((type = parser.next()) == XmlPullParser.TEXT) {
                // no-op
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException(
                        parser.getPositionDescription() + ITEM_MISSING_DRAWABLE_ERROR);
            }
            // Attempt to parse child VDs
            if (parser.getName().equals("vector")) {
                dr = VectorDrawableCompat.createFromXmlInner(resources, parser, attrs, theme);
            } else if (SDK_INT >= LOLLIPOP) {
                dr = Compatibility.Api21Impl.createFromXmlInner(resources, parser, attrs, theme);
            } else {
                dr = Drawable.createFromXmlInner(resources, parser, attrs);
            }
        }
        if (dr == null) {
            throw new XmlPullParserException(
                    parser.getPositionDescription() + ITEM_MISSING_DRAWABLE_ERROR);
        }
        return mState.addStateSet(states, dr, keyframeId);
    }

    @Override
    public Drawable mutate() {
        if (!mMutated && super.mutate() == this) {
            mState.mutate();
            mMutated = true;
        }
        return this;
    }

    @Override
    AnimatedStateListState cloneConstantState() {
        return new AnimatedStateListState(mState, this, null);
    }

    @Override
    void clearMutated() {
        super.clearMutated();
        mMutated = false;
    }

    static class AnimatedStateListState extends StateListState {
        // REVERSED_BIT is indicating the current transition's direction.
        private static final long REVERSED_BIT = 0x100000000L;
        // REVERSIBLE_FLAG_BIT is indicating whether the whole transition has
        // reversible flag set to true.
        private static final long REVERSIBLE_FLAG_BIT = 0x200000000L;
        LongSparseArray<Long> mTransitions;
        SparseArrayCompat<Integer> mStateIds;

        AnimatedStateListState(@Nullable AnimatedStateListState orig,
                @NonNull AnimatedStateListDrawableCompat owner, @Nullable Resources res) {
            super(orig, owner, res);
            if (orig != null) {
                // Perform a shallow copy and rely on mutate() to deep-copy.
                mTransitions = orig.mTransitions;
                mStateIds = orig.mStateIds;
            } else {
                mTransitions = new LongSparseArray<>();
                mStateIds = new SparseArrayCompat<>();
            }
        }

        @Override
        void mutate() {
            mTransitions = mTransitions.clone();
            mStateIds = mStateIds.clone();
        }

        int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) {
            final int pos = super.addChild(anim);
            final long keyFromTo = generateTransitionKey(fromId, toId);
            long reversibleBit = 0;
            if (reversible) {
                reversibleBit = REVERSIBLE_FLAG_BIT;
            }
            mTransitions.append(keyFromTo, pos | reversibleBit);
            if (reversible) {
                final long keyToFrom = generateTransitionKey(toId, fromId);
                mTransitions.append(keyToFrom, pos | REVERSED_BIT | reversibleBit);
            }
            return pos;
        }

        int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
            final int index = super.addStateSet(stateSet, drawable);
            mStateIds.put(index, id);
            return index;
        }

        int indexOfKeyframe(@NonNull int[] stateSet) {
            final int index = super.indexOfStateSet(stateSet);
            if (index >= 0) {
                return index;
            }
            return super.indexOfStateSet(StateSet.WILD_CARD);
        }

        int getKeyframeIdAt(int index) {
            return index < 0 ? 0 : mStateIds.get(index, 0);
        }

        int indexOfTransition(int fromId, int toId) {
            final long keyFromTo = generateTransitionKey(fromId, toId);
            return (int) mTransitions.get(keyFromTo, -1L).longValue();
        }

        boolean isTransitionReversed(int fromId, int toId) {
            final long keyFromTo = generateTransitionKey(fromId, toId);
            return (mTransitions.get(keyFromTo, -1L) & REVERSED_BIT) != 0L;
        }

        boolean transitionHasReversibleFlag(int fromId, int toId) {
            final long keyFromTo = generateTransitionKey(fromId, toId);
            return (mTransitions.get(keyFromTo, -1L) & REVERSIBLE_FLAG_BIT) != 0L;
        }

        @NonNull
        @Override
        public Drawable newDrawable() {
            return new AnimatedStateListDrawableCompat(this, null);
        }

        @NonNull
        @Override
        public Drawable newDrawable(Resources res) {
            return new AnimatedStateListDrawableCompat(this, res);
        }

        private static long generateTransitionKey(int fromId, int toId) {
            return (long) fromId << 32 | toId;
        }
    }

    @Override
    void setConstantState(@NonNull DrawableContainerState state) {
        super.setConstantState(state);
        if (state instanceof AnimatedStateListState) {
            mState = (AnimatedStateListState) state;
        }
    }

    /**
     * Interpolates between frames with respect to their individual durations.
     */
    private static class FrameInterpolator implements TimeInterpolator {
        private int[] mFrameTimes;
        private int mFrames;
        private int mTotalDuration;

        FrameInterpolator(AnimationDrawable d, boolean reversed) {
            updateFrames(d, reversed);
        }

        int updateFrames(AnimationDrawable d, boolean reversed) {
            final int frameCount = d.getNumberOfFrames();
            mFrames = frameCount;
            if (mFrameTimes == null || mFrameTimes.length < frameCount) {
                mFrameTimes = new int[frameCount];
            }
            final int[] frameTimes = mFrameTimes;
            int totalDuration = 0;
            for (int i = 0; i < frameCount; i++) {
                final int duration = d.getDuration(reversed ? frameCount - i - 1 : i);
                frameTimes[i] = duration;
                totalDuration += duration;
            }
            mTotalDuration = totalDuration;
            return totalDuration;
        }

        int getTotalDuration() {
            return mTotalDuration;
        }

        @Override
        public float getInterpolation(float input) {
            final int elapsed = (int) (input * mTotalDuration + 0.5f);
            final int frameCount = mFrames;
            final int[] frameTimes = mFrameTimes;
            // Find the current frame and remaining time within that frame.
            int remaining = elapsed;
            int i = 0;
            while (i < frameCount && remaining >= frameTimes[i]) {
                remaining -= frameTimes[i];
                i++;
            }
            // Remaining time is relative of total duration.
            final float frameElapsed;
            if (i < frameCount) {
                frameElapsed = remaining / (float) mTotalDuration;
            } else {
                frameElapsed = 0;
            }
            return i / (float) frameCount + frameElapsed;
        }
    }
}