AnimatedVectorDrawableCompat.java

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

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;

import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.collection.ArrayMap;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.content.res.TypedArrayUtils;
import androidx.core.graphics.drawable.DrawableCompat;

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

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * For API 24 and above, this class is delegating to the framework's {@link
 * AnimatedVectorDrawable}.
 * For older API version, this class uses {@link android.animation.ObjectAnimator} and
 * {@link android.animation.AnimatorSet} to animate the properties of a
 * {@link VectorDrawableCompat} to create an animated drawable.
 * <p/>
 * AnimatedVectorDrawableCompat are defined in the same XML format as
 * {@link AnimatedVectorDrawable}.
 * <p/>
 * Here are all the animatable attributes in {@link VectorDrawableCompat}:
 * <table border="2" align="center" cellpadding="5">
 *     <thead>
 *         <tr>
 *             <th>Element Name</th>
 *             <th>Animatable attribute name</th>
 *         </tr>
 *     </thead>
 *     <tr>
 *         <td>&lt;vector&gt;</td>
 *         <td>alpha</td>
 *     </tr>
 *     <tr>
 *         <td rowspan="7">&lt;group&gt;</td>
 *         <td>rotation</td>
 *     </tr>
 *     <tr>
 *         <td>pivotX</td>
 *     </tr>
 *     <tr>
 *         <td>pivotY</td>
 *     </tr>
 *     <tr>
 *         <td>scaleX</td>
 *     </tr>
 *     <tr>
 *         <td>scaleY</td>
 *     </tr>
 *     <tr>
 *         <td>translateX</td>
 *     </tr>
 *     <tr>
 *         <td>translateY</td>
 *     </tr>
 *     <tr>
 *         <td rowspan="8">&lt;path&gt;</td>
 *         <td>fillColor</td>
 *     </tr>
 *     <tr>
 *         <td>pathData</td>
 *     </tr>
 *     <tr>
 *         <td>strokeColor</td>
 *     </tr>
 *     <tr>
 *         <td>strokeWidth</td>
 *     </tr>
 *     <tr>
 *         <td>strokeAlpha</td>
 *     </tr>
 *     <tr>
 *         <td>fillAlpha</td>
 *     </tr>
 *     <tr>
 *         <td>trimPathStart</td>
 *     </tr>
 *     <tr>
 *         <td>trimPathEnd</td>
 *     </tr>
 *     <tr>
 *         <td>trimPathOffset</td>
 *     </tr>
 * </table>
 * <p/>
 * You can always create a AnimatedVectorDrawableCompat object and use it as a Drawable by the Java
 * API. In order to refer to AnimatedVectorDrawableCompat inside a XML file, you can use
 * app:srcCompat attribute in AppCompat library's ImageButton or ImageView.
 * <p/>
 * Note that the animation in AnimatedVectorDrawableCompat now can support the following features:
 * <ul>
 * <li>Path Morphing (PathType evaluator). This is used for morphing one path into another.</li>
 * <li>Path Interpolation. This is used to defined a flexible interpolator (represented as a path)
 * instead of the system defined ones like LinearInterpolator.</li>
 * <li>Animating 2 values in one ObjectAnimator according to one path's X value and Y value. One
 * usage is moving one object in both X and Y dimensions along an path.</li>
 * </ul>
 */

public class AnimatedVectorDrawableCompat extends VectorDrawableCommon
        implements Animatable2Compat {
    private static final String LOGTAG = "AnimatedVDCompat";

    private static final String ANIMATED_VECTOR = "animated-vector";
    private static final String TARGET = "target";

    private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;

    private AnimatedVectorDrawableCompatState mAnimatedVectorState;

    private Context mContext;

    private ArgbEvaluator mArgbEvaluator = null;

    AnimatedVectorDrawableDelegateState mCachedConstantStateDelegate;

    // Use internal listener to support AVDC's callback.
    private Animator.AnimatorListener mAnimatorListener = null;

    // Use an array to keep track of multiple call back associated with one drawable.
    ArrayList<Animatable2Compat.AnimationCallback> mAnimationCallbacks = null;


    AnimatedVectorDrawableCompat() {
        this(null, null, null);
    }

    private AnimatedVectorDrawableCompat(@Nullable Context context) {
        this(context, null, null);
    }

    private AnimatedVectorDrawableCompat(@Nullable Context context,
            @Nullable AnimatedVectorDrawableCompatState state,
            @Nullable Resources res) {
        mContext = context;
        if (state != null) {
            mAnimatedVectorState = state;
        } else {
            mAnimatedVectorState = new AnimatedVectorDrawableCompatState(context, state, mCallback,
                    res);
        }
    }

    /**
     * mutate() will be effective only if the getConstantState() is returning non-null.
     * Otherwise, it just return the current object without modification.
     */
    @Override
    public Drawable mutate() {
        if (mDelegateDrawable != null) {
            mDelegateDrawable.mutate();
        }
        // For older platforms that there is no delegated drawable, we just return this without
        // any modification here, and the getConstantState() will return null in this case.
        return this;
    }


    /**
     * Create a AnimatedVectorDrawableCompat object.
     *
     * @param context the context for creating the animators.
     * @param resId   the resource ID for AnimatedVectorDrawableCompat object.
     * @return a new AnimatedVectorDrawableCompat or null if parsing error is found.
     */
    @Nullable
    public static AnimatedVectorDrawableCompat create(@NonNull Context context,
            @DrawableRes int resId) {
        if (Build.VERSION.SDK_INT >= 24) {
            final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context);
            drawable.mDelegateDrawable = ResourcesCompat.getDrawable(context.getResources(), resId,
                    context.getTheme());
            drawable.mDelegateDrawable.setCallback(drawable.mCallback);
            drawable.mCachedConstantStateDelegate = new AnimatedVectorDrawableDelegateState(
                    drawable.mDelegateDrawable.getConstantState());
            return drawable;
        }
        Resources resources = context.getResources();
        try {
            //noinspection AndroidLintResourceType - Parse drawable as XML.
            final XmlPullParser parser = resources.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, context.getResources(), parser, attrs,
                    context.getTheme());
        } catch (XmlPullParserException e) {
            Log.e(LOGTAG, "parser error", e);
        } catch (IOException e) {
            Log.e(LOGTAG, "parser error", e);
        }
        return null;
    }

    /**
     * Create a AnimatedVectorDrawableCompat from inside an XML document using an optional
     * {@link Theme}. Called on a parser positioned at a tag in an XML
     * document, tries to create a Drawable from that tag. Returns {@code null}
     * if the tag is not a valid drawable.
     */
    public static AnimatedVectorDrawableCompat createFromXmlInner(Context context, Resources r,
            XmlPullParser parser, AttributeSet attrs, Theme theme)
            throws XmlPullParserException, IOException {
        final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context);
        drawable.inflate(r, parser, attrs, theme);
        return drawable;
    }

    /**
     * {@inheritDoc}
     * <strong>Note</strong> that we don't support constant state when SDK < 24.
     * Make sure you check the return value before using it.
     */
    @Override
    public ConstantState getConstantState() {
        if (mDelegateDrawable != null && Build.VERSION.SDK_INT >= 24) {
            return new AnimatedVectorDrawableDelegateState(mDelegateDrawable.getConstantState());
        }
        // We can't support constant state in older platform.
        // We need Context to create the animator, and we can't save the context in the constant
        // state.
        return null;
    }

    @Override
    public int getChangingConfigurations() {
        if (mDelegateDrawable != null) {
            return mDelegateDrawable.getChangingConfigurations();
        }
        return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations;
    }

    @Override
    public void draw(Canvas canvas) {
        if (mDelegateDrawable != null) {
            mDelegateDrawable.draw(canvas);
            return;
        }
        mAnimatedVectorState.mVectorDrawable.draw(canvas);
        if (mAnimatedVectorState.mAnimatorSet.isStarted()) {
            invalidateSelf();
        }
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        if (mDelegateDrawable != null) {
            mDelegateDrawable.setBounds(bounds);
            return;
        }
        mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
    }

    @Override
    protected boolean onStateChange(int[] state) {
        if (mDelegateDrawable != null) {
            return mDelegateDrawable.setState(state);
        }
        return mAnimatedVectorState.mVectorDrawable.setState(state);
    }

    @Override
    protected boolean onLevelChange(int level) {
        if (mDelegateDrawable != null) {
            return mDelegateDrawable.setLevel(level);
        }
        return mAnimatedVectorState.mVectorDrawable.setLevel(level);
    }

    @Override
    public int getAlpha() {
        if (mDelegateDrawable != null) {
            return DrawableCompat.getAlpha(mDelegateDrawable);
        }
        return mAnimatedVectorState.mVectorDrawable.getAlpha();
    }

    @Override
    public void setAlpha(int alpha) {
        if (mDelegateDrawable != null) {
            mDelegateDrawable.setAlpha(alpha);
            return;
        }
        mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        if (mDelegateDrawable != null) {
            mDelegateDrawable.setColorFilter(colorFilter);
            return;
        }
        mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
    }

    @Override
    public ColorFilter getColorFilter() {
        if (mDelegateDrawable != null) {
            return DrawableCompat.getColorFilter(mDelegateDrawable);
        }
        return mAnimatedVectorState.mVectorDrawable.getColorFilter();
    }

    @Override
    public void setTint(int tint) {
        if (mDelegateDrawable != null) {
            DrawableCompat.setTint(mDelegateDrawable, tint);
            return;
        }

        mAnimatedVectorState.mVectorDrawable.setTint(tint);
    }

    @Override
    public void setTintList(ColorStateList tint) {
        if (mDelegateDrawable != null) {
            DrawableCompat.setTintList(mDelegateDrawable, tint);
            return;
        }

        mAnimatedVectorState.mVectorDrawable.setTintList(tint);
    }

    @Override
    public void setTintMode(PorterDuff.Mode tintMode) {
        if (mDelegateDrawable != null) {
            DrawableCompat.setTintMode(mDelegateDrawable, tintMode);
            return;
        }

        mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
    }

    @Override
    public boolean setVisible(boolean visible, boolean restart) {
        if (mDelegateDrawable != null) {
            return mDelegateDrawable.setVisible(visible, restart);
        }
        mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
        return super.setVisible(visible, restart);
    }

    @Override
    public boolean isStateful() {
        if (mDelegateDrawable != null) {
            return mDelegateDrawable.isStateful();
        }
        return mAnimatedVectorState.mVectorDrawable.isStateful();
    }

    // Remove deprecation suppression once b/120984759 is resolved
    @SuppressWarnings("deprecation")
    @Override
    public int getOpacity() {
        if (mDelegateDrawable != null) {
            return mDelegateDrawable.getOpacity();
        }
        return mAnimatedVectorState.mVectorDrawable.getOpacity();
    }

    @Override
    public int getIntrinsicWidth() {
        if (mDelegateDrawable != null) {
            return mDelegateDrawable.getIntrinsicWidth();
        }
        return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        if (mDelegateDrawable != null) {
            return mDelegateDrawable.getIntrinsicHeight();
        }
        return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
    }

    @Override
    public boolean isAutoMirrored() {
        if (mDelegateDrawable != null) {
            return DrawableCompat.isAutoMirrored(mDelegateDrawable);
        }
        return mAnimatedVectorState.mVectorDrawable.isAutoMirrored();
    }

    @Override
    public void setAutoMirrored(boolean mirrored) {
        if (mDelegateDrawable != null) {
            DrawableCompat.setAutoMirrored(mDelegateDrawable, mirrored);
            return;
        }
        mAnimatedVectorState.mVectorDrawable.setAutoMirrored(mirrored);
    }

    @Override
    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
            throws XmlPullParserException, IOException {
        if (mDelegateDrawable != null) {
            DrawableCompat.inflate(mDelegateDrawable, res, parser, attrs, theme);
            return;
        }
        int eventType = parser.getEventType();
        final int innerDepth = parser.getDepth() + 1;

        // Parse everything until the end of the animated-vector element.
        while (eventType != XmlPullParser.END_DOCUMENT
                && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
            if (eventType == XmlPullParser.START_TAG) {
                final String tagName = parser.getName();
                if (DBG_ANIMATION_VECTOR_DRAWABLE) {
                    Log.v(LOGTAG, "tagName is " + tagName);
                }
                if (ANIMATED_VECTOR.equals(tagName)) {
                    final TypedArray a =
                            TypedArrayUtils.obtainAttributes(res, theme, attrs,
                                    AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE);

                    int drawableRes = a.getResourceId(
                            AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_DRAWABLE, 0);
                    if (DBG_ANIMATION_VECTOR_DRAWABLE) {
                        Log.v(LOGTAG, "drawableRes is " + drawableRes);
                    }
                    if (drawableRes != 0) {
                        VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(res,
                                drawableRes, theme);
                        vectorDrawable.setAllowCaching(false);
                        vectorDrawable.setCallback(mCallback);
                        if (mAnimatedVectorState.mVectorDrawable != null) {
                            mAnimatedVectorState.mVectorDrawable.setCallback(null);
                        }
                        mAnimatedVectorState.mVectorDrawable = vectorDrawable;
                    }
                    a.recycle();
                } else if (TARGET.equals(tagName)) {
                    final TypedArray a =
                            res.obtainAttributes(attrs,
                                    AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET);
                    final String target = a.getString(
                            AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_NAME);

                    int id = a.getResourceId(
                            AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_ANIMATION,
                            0);
                    if (id != 0) {
                        if (mContext != null) {
                            // There are some important features (like path morphing), added into
                            // Animator code to support AVD at API 21.
                            Animator objectAnimator = AnimatorInflaterCompat.loadAnimator(
                                    mContext, id);
                            setupAnimatorsForTarget(target, objectAnimator);
                        } else {
                            a.recycle();
                            throw new IllegalStateException("Context can't be null when inflating" +
                                    " animators");
                        }
                    }
                    a.recycle();
                }
            }
            eventType = parser.next();
        }

        mAnimatedVectorState.setupAnimatorSet();
    }

    @Override
    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs)
            throws XmlPullParserException, IOException {
        inflate(res, parser, attrs, null);
    }

    @Override
    public void applyTheme(Theme t) {
        if (mDelegateDrawable != null) {
            DrawableCompat.applyTheme(mDelegateDrawable, t);
            return;
        }
        // TODO: support theming in older platform.
        return;
    }

    @Override
    public boolean canApplyTheme() {
        if (mDelegateDrawable != null) {
            return DrawableCompat.canApplyTheme(mDelegateDrawable);
        }
        // TODO: support theming in older platform.
        return false;
    }

    /**
     * Constant state for delegating the creating drawable job.
     * Instead of creating a VectorDrawable, create a VectorDrawableCompat instance which contains
     * a delegated VectorDrawable instance.
     */
    @RequiresApi(24)
    private static class AnimatedVectorDrawableDelegateState extends ConstantState {
        private final ConstantState mDelegateState;

        public AnimatedVectorDrawableDelegateState(ConstantState state) {
            mDelegateState = state;
        }

        @Override
        public Drawable newDrawable() {
            AnimatedVectorDrawableCompat drawableCompat =
                    new AnimatedVectorDrawableCompat();
            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable();
            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
            return drawableCompat;
        }

        @Override
        public Drawable newDrawable(Resources res) {
            AnimatedVectorDrawableCompat drawableCompat =
                    new AnimatedVectorDrawableCompat();
            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res);
            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
            return drawableCompat;
        }

        @Override
        public Drawable newDrawable(Resources res, Theme theme) {
            AnimatedVectorDrawableCompat drawableCompat =
                    new AnimatedVectorDrawableCompat();
            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res, theme);
            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
            return drawableCompat;
        }

        @Override
        public boolean canApplyTheme() {
            return mDelegateState.canApplyTheme();
        }

        @Override
        public int getChangingConfigurations() {
            return mDelegateState.getChangingConfigurations();
        }
    }

    private static class AnimatedVectorDrawableCompatState extends ConstantState {
        int mChangingConfigurations;
        VectorDrawableCompat mVectorDrawable;
        // Combining the array of Animators into a single AnimatorSet to hook up listener easier.
        AnimatorSet mAnimatorSet;
        ArrayList<Animator> mAnimators;
        ArrayMap<Animator, String> mTargetNameMap;

        public AnimatedVectorDrawableCompatState(Context context,
                AnimatedVectorDrawableCompatState copy, Callback owner, Resources res) {
            if (copy != null) {
                mChangingConfigurations = copy.mChangingConfigurations;
                if (copy.mVectorDrawable != null) {
                    final ConstantState cs = copy.mVectorDrawable.getConstantState();
                    if (res != null) {
                        mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(res);
                    } else {
                        mVectorDrawable = (VectorDrawableCompat) cs.newDrawable();
                    }
                    mVectorDrawable = (VectorDrawableCompat) mVectorDrawable.mutate();
                    mVectorDrawable.setCallback(owner);
                    mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
                    mVectorDrawable.setAllowCaching(false);
                }
                if (copy.mAnimators != null) {
                    final int numAnimators = copy.mAnimators.size();
                    mAnimators = new ArrayList<Animator>(numAnimators);
                    mTargetNameMap = new ArrayMap<Animator, String>(numAnimators);
                    for (int i = 0; i < numAnimators; ++i) {
                        Animator anim = copy.mAnimators.get(i);
                        Animator animClone = anim.clone();
                        String targetName = copy.mTargetNameMap.get(anim);
                        Object targetObject = mVectorDrawable.getTargetByName(targetName);
                        animClone.setTarget(targetObject);
                        mAnimators.add(animClone);
                        mTargetNameMap.put(animClone, targetName);
                    }
                    setupAnimatorSet();
                }
            }
        }

        @Override
        public Drawable newDrawable() {
            throw new IllegalStateException("No constant state support for SDK < 24.");
        }

        @Override
        public Drawable newDrawable(Resources res) {
            throw new IllegalStateException("No constant state support for SDK < 24.");
        }

        @Override
        public int getChangingConfigurations() {
            return mChangingConfigurations;
        }

        public void setupAnimatorSet() {
            if (mAnimatorSet == null) {
                mAnimatorSet = new AnimatorSet();
            }
            mAnimatorSet.playTogether(mAnimators);
        }
    }

    /**
     * Utility function to fix color interpolation prior to Lollipop. Without this fix, colors
     * are evaluated as raw integers instead of as colors, which leads to artifacts during
     * fillColor animations.
     */
    private void setupColorAnimator(Animator animator) {
        if (animator instanceof AnimatorSet) {
            List<Animator> childAnimators = ((AnimatorSet) animator).getChildAnimations();
            if (childAnimators != null) {
                for (int i = 0; i < childAnimators.size(); ++i) {
                    setupColorAnimator(childAnimators.get(i));
                }
            }
        }
        if (animator instanceof ObjectAnimator) {
            ObjectAnimator objectAnim = (ObjectAnimator) animator;
            final String propertyName = objectAnim.getPropertyName();
            if ("fillColor".equals(propertyName) || "strokeColor".equals(propertyName)) {
                if (mArgbEvaluator == null) {
                    mArgbEvaluator = new ArgbEvaluator();
                }
                objectAnim.setEvaluator(mArgbEvaluator);
            }
        }
    }

    private void setupAnimatorsForTarget(String name, Animator animator) {
        Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name);
        animator.setTarget(target);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            setupColorAnimator(animator);
        }
        if (mAnimatedVectorState.mAnimators == null) {
            mAnimatedVectorState.mAnimators = new ArrayList<Animator>();
            mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>();
        }
        mAnimatedVectorState.mAnimators.add(animator);
        mAnimatedVectorState.mTargetNameMap.put(animator, name);
        if (DBG_ANIMATION_VECTOR_DRAWABLE) {
            Log.v(LOGTAG, "add animator  for target " + name + " " + animator);
        }
    }

    @Override
    public boolean isRunning() {
        if (mDelegateDrawable != null) {
            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
            return ((AnimatedVectorDrawable) mDelegateDrawable).isRunning();
        }
        return mAnimatedVectorState.mAnimatorSet.isRunning();
    }

    @Override
    public void start() {
        if (mDelegateDrawable != null) {
            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
            ((AnimatedVectorDrawable) mDelegateDrawable).start();
            return;
        }
        // If any one of the animator has not ended, do nothing.
        if (mAnimatedVectorState.mAnimatorSet.isStarted()) {
            return;
        }
        // Otherwise, kick off animatorSet.
        mAnimatedVectorState.mAnimatorSet.start();
        invalidateSelf();
    }

    @Override
    public void stop() {
        if (mDelegateDrawable != null) {
            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
            ((AnimatedVectorDrawable) mDelegateDrawable).stop();
            return;
        }
        mAnimatedVectorState.mAnimatorSet.end();
    }

    final Callback mCallback = new Callback() {
        @Override
        public void invalidateDrawable(Drawable who) {
            invalidateSelf();
        }

        @Override
        public void scheduleDrawable(Drawable who, Runnable what, long when) {
            scheduleSelf(what, when);
        }

        @Override
        public void unscheduleDrawable(Drawable who, Runnable what) {
            unscheduleSelf(what);
        }
    };

    /**
     * A helper function to unregister the Animatable2Compat callback from the platform's
     * Animatable2 callback, while keeping the internal array of callback up to date.
     */
    @RequiresApi(23)
    private static boolean unregisterPlatformCallback(AnimatedVectorDrawable dr,
            Animatable2Compat.AnimationCallback callback) {
        return dr.unregisterAnimationCallback(callback.getPlatformCallback());
    }

    @Override
    public void registerAnimationCallback(@NonNull Animatable2Compat.AnimationCallback
            callback) {
        if (mDelegateDrawable != null) {
            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
            registerPlatformCallback((AnimatedVectorDrawable) mDelegateDrawable, callback);
            return;
        }

        if (callback == null) {
            return;
        }

        // Add listener accordingly.
        if (mAnimationCallbacks == null) {
            mAnimationCallbacks = new ArrayList<>();
        }

        if (mAnimationCallbacks.contains(callback)) {
            // If this call back is already in, then don't need to append another copy.
            return;
        }

        mAnimationCallbacks.add(callback);

        if (mAnimatorListener == null) {
            // Create a animator listener and trigger the callback events when listener is
            // triggered.
            mAnimatorListener = new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
                    ArrayList<Animatable2Compat.AnimationCallback> tmpCallbacks =
                            new ArrayList<>(mAnimationCallbacks);
                    int size = tmpCallbacks.size();
                    for (int i = 0; i < size; i++) {
                        tmpCallbacks.get(i).onAnimationStart(AnimatedVectorDrawableCompat.this);
                    }
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    ArrayList<Animatable2Compat.AnimationCallback> tmpCallbacks =
                            new ArrayList<>(mAnimationCallbacks);
                    int size = tmpCallbacks.size();
                    for (int i = 0; i < size; i++) {
                        tmpCallbacks.get(i).onAnimationEnd(AnimatedVectorDrawableCompat.this);
                    }
                }
            };
        }
        mAnimatedVectorState.mAnimatorSet.addListener(mAnimatorListener);
    }

    /**
     * A helper function to register the Animatable2Compat callback on the platform's Animatable2
     * callback.
     */
    @RequiresApi(23)
    private static void registerPlatformCallback(@NonNull AnimatedVectorDrawable avd,
            @NonNull final Animatable2Compat.AnimationCallback callback) {
        avd.registerAnimationCallback(callback.getPlatformCallback());
    }

    /**
     * A helper function to clean up the animator listener in the mAnimatorSet.
     */
    private void removeAnimatorSetListener() {
        if (mAnimatorListener != null) {
            mAnimatedVectorState.mAnimatorSet.removeListener(mAnimatorListener);
            mAnimatorListener = null;
        }
    }

    @Override
    public boolean unregisterAnimationCallback(
            @NonNull Animatable2Compat.AnimationCallback callback) {
        if (mDelegateDrawable != null) {
            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
            unregisterPlatformCallback((AnimatedVectorDrawable) mDelegateDrawable, callback);
        }

        if (mAnimationCallbacks == null || callback == null) {
            // Nothing to be removed.
            return false;
        }
        boolean removed = mAnimationCallbacks.remove(callback);

        //  When the last call back unregistered, remove the listener accordingly.
        if (mAnimationCallbacks.size() == 0) {
            removeAnimatorSetListener();
        }
        return removed;
    }

    @Override
    public void clearAnimationCallbacks() {
        if (mDelegateDrawable != null) {
            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
            ((AnimatedVectorDrawable) mDelegateDrawable).clearAnimationCallbacks();
            return;
        }
        removeAnimatorSetListener();
        if (mAnimationCallbacks == null) {
            return;
        }

        mAnimationCallbacks.clear();
    }

    /**
     * Utility function to register callback to Drawable, when the drawable is created from XML and
     * referred in Java code, e.g: ImageView.getDrawable().
     * From API 24 on, the drawable is treated as an AnimatedVectorDrawable.
     * Otherwise, it is treated as AnimatedVectorDrawableCompat.
     */
    public static void registerAnimationCallback(Drawable dr,
            Animatable2Compat.AnimationCallback callback) {
        if (dr == null || callback == null) {
            return;
        }
        if (!(dr instanceof Animatable)) {
            return;
        }

        if (Build.VERSION.SDK_INT >= 24) {
            registerPlatformCallback((AnimatedVectorDrawable) dr, callback);
        } else {
            ((AnimatedVectorDrawableCompat) dr).registerAnimationCallback(callback);
        }
    }

    /**
     * Utility function to unregister animation callback from Drawable, when the drawable is
     * created from XML and referred in Java code, e.g: ImageView.getDrawable().
     * From API 24 on, the drawable is treated as an AnimatedVectorDrawable.
     * Otherwise, it is treated as AnimatedVectorDrawableCompat.
     */
    public static boolean unregisterAnimationCallback(Drawable dr,
            Animatable2Compat.AnimationCallback callback) {
        if (dr == null || callback == null) {
            return false;
        }
        if (!(dr instanceof Animatable)) {
            return false;
        }

        if (Build.VERSION.SDK_INT >= 24) {
            return unregisterPlatformCallback((AnimatedVectorDrawable) dr, callback);
        } else {
            return ((AnimatedVectorDrawableCompat) dr).unregisterAnimationCallback(callback);
        }
    }

    /**
     * Utility function to clear animation callbacks from Drawable, when the drawable is
     * created from XML and referred in Java code, e.g: ImageView.getDrawable().
     * From API 24 on, the drawable is treated as an AnimatedVectorDrawable.
     * Otherwise, it is treated as AnimatedVectorDrawableCompat.
     */
    public static void clearAnimationCallbacks(Drawable dr) {
        if (!(dr instanceof Animatable)) {
            return;
        }
        if (Build.VERSION.SDK_INT >= 24) {
            ((AnimatedVectorDrawable) dr).clearAnimationCallbacks();
        } else {
            ((AnimatedVectorDrawableCompat) dr).clearAnimationCallbacks();
        }

    }
}