public class

SeekableAnimatedVectorDrawable

extends Drawable

 java.lang.Object

↳Drawable

↳androidx.vectordrawable.graphics.drawable.SeekableAnimatedVectorDrawable

Gradle dependencies

compile group: 'androidx.vectordrawable', name: 'vectordrawable-seekable', version: '1.0.0'

  • groupId: androidx.vectordrawable
  • artifactId: vectordrawable-seekable
  • version: 1.0.0

Artifact androidx.vectordrawable:vectordrawable-seekable:1.0.0 it located at Google repository (https://maven.google.com/)

Overview

This class animates properties of a VectorDrawableCompat with animations defined using ObjectAnimator or AnimatorSet.

SeekableAnimatedVectorDrawable is defined in the same XML format as .

Here are all the animatable attributes in VectorDrawableCompat:

Element Name Animatable attribute name
<vector> alpha
<group> rotation
pivotX
pivotY
scaleX
scaleY
translateX
translateY
<path> fillColor
pathData
strokeColor
strokeWidth
strokeAlpha
fillAlpha
trimPathStart
trimPathEnd
trimPathOffset

You can always create a SeekableAnimatedVectorDrawable object and use it as a Drawable by the Java API. In order to refer to SeekableAnimatedVectorDrawable inside an XML file, you can use app:srcCompat attribute in AppCompat library's ImageButton or ImageView.

SeekableAnimatedVectorDrawable supports the following features too:

  • Path Morphing (PathType evaluator). This is used for morphing one path into another.
  • Path Interpolation. This is used to defined a flexible interpolator (represented as a path) instead of the system defined ones like LinearInterpolator.
  • 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.

Unlike AnimatedVectorDrawableCompat, this class does not delegate to the platform on any API levels.

Summary

Methods
public voidapplyTheme(Theme t)

public booleancanApplyTheme()

public voidclearAnimationCallbacks()

Removes all existing animation callbacks.

public static SeekableAnimatedVectorDrawablecreate(Context context, int resId)

Create a SeekableAnimatedVectorDrawable object.

public static SeekableAnimatedVectorDrawablecreateFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)

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

public voiddraw(Canvas canvas)

public intgetAlpha()

public intgetChangingConfigurations()

public ColorFiltergetColorFilter()

public ConstantStategetConstantState()

public longgetCurrentPlayTime()

Returns the milliseconds elapsed since the start of the animation.

public intgetIntrinsicHeight()

public intgetIntrinsicWidth()

public intgetOpacity()

public longgetTotalDuration()

Gets the total duration of the animation, accounting for animation sequences, start delay, and repeating.

public voidinflate(Resources res, XmlPullParser parser, AttributeSet attrs)

public voidinflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)

public booleanisAutoMirrored()

public booleanisPaused()

Returns whether the animation is currently in a paused state.

public booleanisRunning()

Returns whether the animation is running (has started and not yet ended).

public booleanisStateful()

public Drawablemutate()

mutate() is not supported.

protected voidonBoundsChange(Rect bounds)

protected booleanonLevelChange(int level)

protected booleanonStateChange(int[] state[])

public voidpause()

Pauses a running animation.

public voidregisterAnimationCallback(SeekableAnimatedVectorDrawable.AnimationCallback callback)

Adds a callback to listen to the animation events.

public voidresume()

Resumes a paused animation.

public voidsetAlpha(int alpha)

public voidsetAutoMirrored(boolean mirrored)

public voidsetColorFilter(ColorFilter colorFilter)

public voidsetCurrentPlayTime(long playTime)

Sets the position of the animation to the specified point in time.

public voidsetTint(int tint)

public voidsetTintList(ColorStateList tint)

public voidsetTintMode(PorterDuff.Mode tintMode)

public booleansetVisible(boolean visible, boolean restart)

public voidstart()

public voidstop()

public booleanunregisterAnimationCallback(SeekableAnimatedVectorDrawable.AnimationCallback callback)

Removes the specified animation callback.

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

Methods

public Drawable mutate()

mutate() is not supported. This method simply returns this.

public static SeekableAnimatedVectorDrawable create(Context context, int resId)

Create a SeekableAnimatedVectorDrawable object.

Parameters:

context: the context for creating the animators.
resId: the resource ID for SeekableAnimatedVectorDrawable object.

Returns:

a new SeekableAnimatedVectorDrawable or null if parsing error is found.

public static SeekableAnimatedVectorDrawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)

Create a SeekableAnimatedVectorDrawable from inside an XML document using an optional . Called on a parser positioned at a tag in an XML document, tries to create a Drawable from that tag. Returns null if the tag is not a valid drawable.

public ConstantState getConstantState()

public int getChangingConfigurations()

public void draw(Canvas canvas)

protected void onBoundsChange(Rect bounds)

protected boolean onStateChange(int[] state[])

protected boolean onLevelChange(int level)

public int getAlpha()

public void setAlpha(int alpha)

public void setColorFilter(ColorFilter colorFilter)

public ColorFilter getColorFilter()

public void setTint(int tint)

public void setTintList(ColorStateList tint)

public void setTintMode(PorterDuff.Mode tintMode)

public boolean setVisible(boolean visible, boolean restart)

public boolean isStateful()

public int getOpacity()

Deprecated: This method is no longer used in graphics optimizations

Returns:

The opacity class of the Drawable.

public int getIntrinsicWidth()

public int getIntrinsicHeight()

public boolean isAutoMirrored()

public void setAutoMirrored(boolean mirrored)

public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)

public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs)

public void applyTheme(Theme t)

public boolean canApplyTheme()

public boolean isRunning()

Returns whether the animation is running (has started and not yet ended).

Returns:

true if the animation is running.

public boolean isPaused()

Returns whether the animation is currently in a paused state.

Returns:

true if the animation is paused.

public void start()

public void stop()

public void pause()

Pauses a running animation. This method should only be called on the same thread on which the animation was started. If the animation has not yet been started or has since ended, then the call is ignored. Paused animations can be resumed by calling SeekableAnimatedVectorDrawable.resume().

public void resume()

Resumes a paused animation. The animation resumes from where it left off when it was paused. This method should only be called on the same thread on which the animation was started. Calls will be ignored if this SeekableAnimatedVectorDrawable is not currently paused.

public void setCurrentPlayTime(long playTime)

Sets the position of the animation to the specified point in time. This time should be between 0 and the total duration of the animation, including any repetition. If the animation has not yet been started, then it will not advance forward after it is set to this time; it will simply set the time to this value and perform any appropriate actions based on that time. If the animation is already running, then setCurrentPlayTime() will set the current playing time to this value and continue playing from that point.

Parameters:

playTime: The time, in milliseconds, to which the animation is advanced or rewound. Unless the animation is reversing, the playtime is considered the time since the end of the start delay of the AnimatorSet in a forward playing direction.

public long getCurrentPlayTime()

Returns the milliseconds elapsed since the start of the animation.

For ongoing animations, this method returns the current progress of the animation in terms of play time. For an animation that has not yet been started: if the animation has been seeked to a certain time via SeekableAnimatedVectorDrawable.setCurrentPlayTime(long), the seeked play time will be returned; otherwise, this method will return 0.

Returns:

the current position in time of the animation in milliseconds

public long getTotalDuration()

Gets the total duration of the animation, accounting for animation sequences, start delay, and repeating. Return Animator.DURATION_INFINITE if the duration is infinite.

Returns:

Total time the animation takes to finish, starting from the time SeekableAnimatedVectorDrawable.start() is called. Animator.DURATION_INFINITE if the animation or any of the child animations repeats infinite times.

public void registerAnimationCallback(SeekableAnimatedVectorDrawable.AnimationCallback callback)

Adds a callback to listen to the animation events.

Parameters:

callback: Callback to add.

public boolean unregisterAnimationCallback(SeekableAnimatedVectorDrawable.AnimationCallback callback)

Removes the specified animation callback.

Parameters:

callback: Callback to remove.

Returns:

false if callback didn't exist in the call back list, or true if callback has been removed successfully.

public void clearAnimationCallbacks()

Removes all existing animation callbacks.

Source

/*
 * Copyright 2020 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.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.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;

import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.SimpleArrayMap;
import androidx.core.animation.Animator;
import androidx.core.animation.AnimatorInflater;
import androidx.core.animation.AnimatorListenerAdapter;
import androidx.core.animation.AnimatorSet;
import androidx.core.animation.ObjectAnimator;
import androidx.core.content.res.TypedArrayUtils;

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

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

/**
 * This class animates properties of a {@link VectorDrawableCompat} with animations defined using
 * {@link ObjectAnimator} or {@link AnimatorSet}.
 *
 * <p>
 * SeekableAnimatedVectorDrawable is defined in the same XML format as
 * {@link android.graphics.drawable.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 SeekableAnimatedVectorDrawable object and use it as a Drawable by the
 * Java API. In order to refer to SeekableAnimatedVectorDrawable inside an XML file, you can
 * use app:srcCompat attribute in AppCompat library's ImageButton or ImageView.
 * <p>
 * SeekableAnimatedVectorDrawable supports the following features too:
 * <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>
 * <p>
 * Unlike {@code AnimatedVectorDrawableCompat}, this class does not delegate to the platform
 * {@link android.graphics.drawable.AnimatedVectorDrawable} on any API levels.
 */
public class SeekableAnimatedVectorDrawable extends Drawable implements Animatable {

    private static final String LOGTAG = "SeekableAVD";

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

    private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;

    private AnimatedVectorDrawableState mAnimatedVectorState;

    // An internal listener to bridge between Animator and SAVD's callbacks.
    private InternalAnimatorListener mAnimatorListener = null;

    // An array to keep track of multiple callbacks associated with one drawable.
    @SuppressWarnings("WeakerAccess")
    ArrayList<AnimationCallback> mAnimationCallbacks = null;

    /**
     * Abstract class for animation callback. Used to notify animation events.
     */
    public abstract static class AnimationCallback {

        /**
         * Called when the animation starts.
         *
         * @param drawable The drawable started the animation.
         */
        public void onAnimationStart(@NonNull SeekableAnimatedVectorDrawable drawable) {
        }

        /**
         * Called when the animation ends.
         *
         * @param drawable The drawable finished the animation.
         */
        public void onAnimationEnd(@NonNull SeekableAnimatedVectorDrawable drawable) {
        }

        /**
         * Called when the animation is paused.
         *
         * @param drawable The drawable.
         */
        public void onAnimationPause(@NonNull SeekableAnimatedVectorDrawable drawable) {
        }

        /**
         * Called when the animation is resumed.
         *
         * @param drawable The drawable.
         */
        public void onAnimationResume(@NonNull SeekableAnimatedVectorDrawable drawable) {
        }

        /**
         * Called on every frame while the animation is running. The implementation must not
         * register or unregister any {@link AnimationCallback} here.
         *
         * @param drawable The drawable.
         */
        public void onAnimationUpdate(@NonNull SeekableAnimatedVectorDrawable drawable) {
        }
    }

    private final Callback mCallback = new Callback() {

        @Override
        public void invalidateDrawable(@NonNull Drawable who) {
            invalidateSelf();
        }

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

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

    private SeekableAnimatedVectorDrawable() {
        this(null, null);
    }

    private SeekableAnimatedVectorDrawable(
            @Nullable AnimatedVectorDrawableState state,
            @Nullable Resources res
    ) {
        if (state != null) {
            mAnimatedVectorState = state;
        } else {
            mAnimatedVectorState =
                    new AnimatedVectorDrawableState(null, mCallback, res);
        }
    }

    /**
     * mutate() is not supported. This method simply returns {@code this}.
     */
    @NonNull
    @Override
    public Drawable mutate() {
        return this;
    }

    /**
     * Create a SeekableAnimatedVectorDrawable object.
     *
     * @param context the context for creating the animators.
     * @param resId   the resource ID for SeekableAnimatedVectorDrawable object.
     * @return a new SeekableAnimatedVectorDrawable or null if parsing error is found.
     */
    @Nullable
    public static SeekableAnimatedVectorDrawable create(
            @NonNull Context context,
            @DrawableRes int resId
    ) {
        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;
            do {
                type = parser.next();
            } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }
            return createFromXmlInner(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 SeekableAnimatedVectorDrawable 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.
     */
    @NonNull
    public static SeekableAnimatedVectorDrawable createFromXmlInner(
            @NonNull Resources r,
            @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs,
            @Nullable Theme theme
    ) throws XmlPullParserException, IOException {
        final SeekableAnimatedVectorDrawable drawable = new SeekableAnimatedVectorDrawable();
        drawable.inflate(r, parser, attrs, theme);
        return drawable;
    }

    @Nullable
    @Override
    public ConstantState 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() {
        return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations;
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        mAnimatedVectorState.mVectorDrawable.draw(canvas);
        if (mAnimatedVectorState.mAnimatorSet.isStarted()) {
            invalidateSelf();
        }
    }

    @Override
    protected void onBoundsChange(@NonNull Rect bounds) {
        mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
    }

    @Override
    protected boolean onStateChange(@NonNull int[] state) {
        return mAnimatedVectorState.mVectorDrawable.setState(state);
    }

    @Override
    protected boolean onLevelChange(int level) {
        return mAnimatedVectorState.mVectorDrawable.setLevel(level);
    }

    @IntRange(from = 0, to = 255)
    @Override
    public int getAlpha() {
        return mAnimatedVectorState.mVectorDrawable.getAlpha();
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
        mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
    }

    @Nullable
    @Override
    public ColorFilter getColorFilter() {
        return mAnimatedVectorState.mVectorDrawable.getColorFilter();
    }

    @Override
    public void setTint(@ColorInt int tint) {
        mAnimatedVectorState.mVectorDrawable.setTint(tint);
    }

    @Override
    public void setTintList(@Nullable ColorStateList tint) {
        mAnimatedVectorState.mVectorDrawable.setTintList(tint);
    }

    @Override
    public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
        mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
    }

    @Override
    public boolean setVisible(boolean visible, boolean restart) {
        mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
        return super.setVisible(visible, restart);
    }

    @Override
    public boolean isStateful() {
        return mAnimatedVectorState.mVectorDrawable.isStateful();
    }

    /**
     * @return The opacity class of the Drawable.
     * @deprecated This method is no longer used in graphics optimizations
     */
    @Deprecated
    @Override
    public int getOpacity() {
        return mAnimatedVectorState.mVectorDrawable.getOpacity();
    }

    @Override
    public int getIntrinsicWidth() {
        return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
    }

    @Override
    public boolean isAutoMirrored() {
        return mAnimatedVectorState.mVectorDrawable.isAutoMirrored();
    }

    @Override
    public void setAutoMirrored(boolean mirrored) {
        mAnimatedVectorState.mVectorDrawable.setAutoMirrored(mirrored);
    }

    @Override
    public void inflate(
            @NonNull Resources res,
            @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs,
            @Nullable Theme theme
    ) throws XmlPullParserException, IOException {
        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.createWithoutDelegate(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) {
                        // There are some important features (like path morphing), added into
                        // Animator code to support AVD at API 21.
                        Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id);
                        setupAnimatorsForTarget(target, objectAnimator);
                    }
                    a.recycle();
                }
            }
            eventType = parser.next();
        }

        mAnimatedVectorState.setupAnimatorSet();
    }

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

    @Override
    public void applyTheme(@NonNull Theme t) {
        // TODO(b/149342571): support theming in older platform.
    }

    @Override
    public boolean canApplyTheme() {
        // TODO(b/149342571): support theming in older platform.
        return false;
    }

    private static class AnimatedVectorDrawableState 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;
        SimpleArrayMap<Animator, String> mTargetNameMap;

        AnimatedVectorDrawableState(
                AnimatedVectorDrawableState 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<>(numAnimators);
                    mTargetNameMap = new SimpleArrayMap<>(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();
                }
            }
        }

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

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

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

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

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

    /**
     * Returns whether the animation is running (has started and not yet ended).
     *
     * @return {@code true} if the animation is running.
     */
    @Override
    public boolean isRunning() {
        return mAnimatedVectorState.mAnimatorSet.isRunning();
    }

    /**
     * Returns whether the animation is currently in a paused state.
     *
     * @return {@code true} if the animation is paused.
     */
    public boolean isPaused() {
        return mAnimatedVectorState.mAnimatorSet.isPaused();
    }

    @Override
    public void start() {
        // 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() {
        mAnimatedVectorState.mAnimatorSet.end();
    }

    /**
     * Pauses a running animation. This method should only be called on the same thread on which
     * the animation was started. If the animation has not yet been started or has since ended,
     * then the call is ignored. Paused animations can be resumed by calling {@link #resume()}.
     */
    public void pause() {
        mAnimatedVectorState.mAnimatorSet.pause();
    }

    /**
     * Resumes a paused animation. The animation resumes from where it left off when it was
     * paused. This method should only be called on the same thread on which the animation was
     * started. Calls will be ignored if this {@link SeekableAnimatedVectorDrawable} is not
     * currently paused.
     */
    public void resume() {
        mAnimatedVectorState.mAnimatorSet.resume();
    }

    /**
     * Sets the position of the animation to the specified point in time. This time should be
     * between 0 and the total duration of the animation, including any repetition. If the
     * animation has not yet been started, then it will not advance forward after it is set to this
     * time; it will simply set the time to this value and perform any appropriate actions based on
     * that time. If the animation is already running, then setCurrentPlayTime() will set the
     * current playing time to this value and continue playing from that point.
     *
     * @param playTime The time, in milliseconds, to which the animation is advanced or rewound.
     *                 Unless the animation is reversing, the playtime is considered the time since
     *                 the end of the start delay of the AnimatorSet in a forward playing direction.
     */
    public void setCurrentPlayTime(@IntRange(from = 0) long playTime) {
        mAnimatedVectorState.mAnimatorSet.setCurrentPlayTime(playTime);
        invalidateSelf();
    }

    /**
     * Returns the milliseconds elapsed since the start of the animation.
     *
     * <p>For ongoing animations, this method returns the current progress of the animation in
     * terms of play time. For an animation that has not yet been started: if the animation has been
     * seeked to a certain time via {@link #setCurrentPlayTime(long)}, the seeked play time will
     * be returned; otherwise, this method will return 0.
     *
     * @return the current position in time of the animation in milliseconds
     */
    @IntRange(from = 0)
    public long getCurrentPlayTime() {
        return mAnimatedVectorState.mAnimatorSet.getCurrentPlayTime();
    }

    /**
     * Gets the total duration of the animation, accounting for animation sequences, start delay,
     * and repeating. Return {@link Animator#DURATION_INFINITE} if the duration is infinite.
     *
     * @return Total time the animation takes to finish, starting from the time {@link #start()}
     * is called. {@link Animator#DURATION_INFINITE} if the animation or any of the child
     * animations repeats infinite times.
     */
    public long getTotalDuration() {
        return mAnimatedVectorState.mAnimatorSet.getTotalDuration();
    }

    class InternalAnimatorListener extends AnimatorListenerAdapter
            implements Animator.AnimatorUpdateListener {

        @Override
        public void onAnimationStart(@NonNull Animator animation) {
            final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
            if (callbacks != null) {
                for (int i = 0, size = callbacks.size(); i < size; i++) {
                    callbacks.get(i).onAnimationStart(SeekableAnimatedVectorDrawable.this);
                }
            }
        }

        @Override
        public void onAnimationEnd(@NonNull Animator animation) {
            final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
            if (callbacks != null) {
                for (int i = 0, size = callbacks.size(); i < size; i++) {
                    callbacks.get(i).onAnimationEnd(SeekableAnimatedVectorDrawable.this);
                }
            }
        }

        @Override
        public void onAnimationPause(@NonNull Animator animation) {
            final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
            if (callbacks != null) {
                for (int i = 0, size = callbacks.size(); i < size; i++) {
                    callbacks.get(i).onAnimationPause(SeekableAnimatedVectorDrawable.this);
                }
            }
        }

        @Override
        public void onAnimationResume(@NonNull Animator animation) {
            final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
            if (callbacks != null) {
                for (int i = 0, size = callbacks.size(); i < size; i++) {
                    callbacks.get(i).onAnimationResume(SeekableAnimatedVectorDrawable.this);
                }
            }
        }

        @Override
        public void onAnimationUpdate(@NonNull Animator animation) {
            final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
            if (callbacks != null) {
                for (int i = 0, size = callbacks.size(); i < size; i++) {
                    callbacks.get(i).onAnimationUpdate(SeekableAnimatedVectorDrawable.this);
                }
            }
        }
    }

    /**
     * Adds a callback to listen to the animation events.
     *
     * @param callback Callback to add.
     */
    public void registerAnimationCallback(@NonNull AnimationCallback callback) {
        // Add listener accordingly.
        if (mAnimationCallbacks == null) {
            mAnimationCallbacks = new ArrayList<>();
        } else if (mAnimationCallbacks.contains(callback)) {
            // If this call back is already in, then don't need to append another copy.
            return;
        } else {
            mAnimationCallbacks = new ArrayList<>(mAnimationCallbacks);
        }

        mAnimationCallbacks.add(callback);

        if (mAnimatorListener == null) {
            // Create an internal listener in order to bridge events to our callbacks.
            mAnimatorListener = new InternalAnimatorListener();
            mAnimatedVectorState.mAnimatorSet.addListener(mAnimatorListener);
            mAnimatedVectorState.mAnimatorSet.addPauseListener(mAnimatorListener);
            mAnimatedVectorState.mAnimatorSet.addUpdateListener(mAnimatorListener);
        }
    }

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

    /**
     * Removes the specified animation callback.
     *
     * @param callback Callback to remove.
     * @return {@code false} if callback didn't exist in the call back list, or {@code true} if
     * callback has been removed successfully.
     */
    public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) {
        if (mAnimationCallbacks == null) {
            // Nothing to be removed.
            return false;
        }

        boolean removed = false;
        if (mAnimationCallbacks.contains(callback)) {
            mAnimationCallbacks = new ArrayList<>(mAnimationCallbacks);
            mAnimationCallbacks.remove(callback);
            removed = true;
        }

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

    /**
     * Removes all existing animation callbacks.
     */
    public void clearAnimationCallbacks() {
        removeAnimatorSetListener();
        if (mAnimationCallbacks == null) {
            return;
        }
        mAnimationCallbacks.clear();
    }
}