AnimatedVectorDrawable.java revision fc8e3cb768b7dfd7c0ed0fb93dd9d735887e8d45
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14
15package android.graphics.drawable;
16
17import android.animation.Animator;
18import android.animation.AnimatorInflater;
19import android.animation.AnimatorListenerAdapter;
20import android.animation.AnimatorSet;
21import android.animation.Animator.AnimatorListener;
22import android.animation.ValueAnimator;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.content.res.ColorStateList;
26import android.content.res.Resources;
27import android.content.res.Resources.Theme;
28import android.content.res.TypedArray;
29import android.graphics.Canvas;
30import android.graphics.ColorFilter;
31import android.graphics.Insets;
32import android.graphics.Outline;
33import android.graphics.PorterDuff;
34import android.graphics.Rect;
35import android.util.ArrayMap;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.view.View;
39
40import com.android.internal.R;
41
42import org.xmlpull.v1.XmlPullParser;
43import org.xmlpull.v1.XmlPullParserException;
44
45import java.io.IOException;
46import java.util.ArrayList;
47
48/**
49 * This class uses {@link android.animation.ObjectAnimator} and
50 * {@link android.animation.AnimatorSet} to animate the properties of a
51 * {@link android.graphics.drawable.VectorDrawable} to create an animated drawable.
52 * <p>
53 * AnimatedVectorDrawable are normally defined as 3 separate XML files.
54 * </p>
55 * <p>
56 * First is the XML file for {@link android.graphics.drawable.VectorDrawable}.
57 * Note that we allow the animation to happen on the group's attributes and path's
58 * attributes, which requires they are uniquely named in this XML file. Groups
59 * and paths without animations do not need names.
60 * </p>
61 * <li>Here is a simple VectorDrawable in this vectordrawable.xml file.
62 * <pre>
63 * &lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android";
64 *     android:height=&quot;64dp&quot;
65 *     android:width=&quot;64dp&quot;
66 *     android:viewportHeight=&quot;600&quot;
67 *     android:viewportWidth=&quot;600&quot; &gt;
68 *     &lt;group
69 *         android:name=&quot;rotationGroup&quot;
70 *         android:pivotX=&quot;300.0&quot;
71 *         android:pivotY=&quot;300.0&quot;
72 *         android:rotation=&quot;45.0&quot; &gt;
73 *         &lt;path
74 *             android:name=&quot;v&quot;
75 *             android:fillColor=&quot;#000000&quot;
76 *             android:pathData=&quot;M300,70 l 0,-70 70,70 0,0 -70,70z&quot; /&gt;
77 *     &lt;/group&gt;
78 * &lt;/vector&gt;
79 * </pre></li>
80 * <p>
81 * Second is the AnimatedVectorDrawable's XML file, which defines the target
82 * VectorDrawable, the target paths and groups to animate, the properties of the
83 * path and group to animate and the animations defined as the ObjectAnimators
84 * or AnimatorSets.
85 * </p>
86 * <li>Here is a simple AnimatedVectorDrawable defined in this avd.xml file.
87 * Note how we use the names to refer to the groups and paths in the vectordrawable.xml.
88 * <pre>
89 * &lt;animated-vector xmlns:android=&quot;http://schemas.android.com/apk/res/android";
90 *   android:drawable=&quot;@drawable/vectordrawable&quot; &gt;
91 *     &lt;target
92 *         android:name=&quot;rotationGroup&quot;
93 *         android:animation=&quot;@anim/rotation&quot; /&gt;
94 *     &lt;target
95 *         android:name=&quot;v&quot;
96 *         android:animation=&quot;@anim/path_morph&quot; /&gt;
97 * &lt;/animated-vector&gt;
98 * </pre></li>
99 * <p>
100 * Last is the Animator XML file, which is the same as a normal ObjectAnimator
101 * or AnimatorSet.
102 * To complete this example, here are the 2 animator files used in avd.xml:
103 * rotation.xml and path_morph.xml.
104 * </p>
105 * <li>Here is the rotation.xml, which will rotate the target group for 360 degrees.
106 * <pre>
107 * &lt;objectAnimator
108 *     android:duration=&quot;6000&quot;
109 *     android:propertyName=&quot;rotation&quot;
110 *     android:valueFrom=&quot;0&quot;
111 *     android:valueTo=&quot;360&quot; /&gt;
112 * </pre></li>
113 * <li>Here is the path_morph.xml, which will morph the path from one shape to
114 * the other. Note that the paths must be compatible for morphing.
115 * In more details, the paths should have exact same length of commands , and
116 * exact same length of parameters for each commands.
117 * Note that the path strings are better stored in strings.xml for reusing.
118 * <pre>
119 * &lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android">;
120 *     &lt;objectAnimator
121 *         android:duration=&quot;3000&quot;
122 *         android:propertyName=&quot;pathData&quot;
123 *         android:valueFrom=&quot;M300,70 l 0,-70 70,70 0,0   -70,70z&quot;
124 *         android:valueTo=&quot;M300,70 l 0,-70 70,0  0,140 -70,0 z&quot;
125 *         android:valueType=&quot;pathType&quot;/&gt;
126 * &lt;/set&gt;
127 * </pre></li>
128 *
129 * @attr ref android.R.styleable#AnimatedVectorDrawable_drawable
130 * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name
131 * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation
132 */
133public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
134    private static final String LOGTAG = "AnimatedVectorDrawable";
135
136    private static final String ANIMATED_VECTOR = "animated-vector";
137    private static final String TARGET = "target";
138
139    private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
140
141    /** Local, mutable animator set. */
142    private final AnimatorSet mAnimatorSet = new AnimatorSet();
143
144    // Setup a value animator to get animation update callbacks.
145    private final ValueAnimator mUpdateAnim = ValueAnimator.ofFloat(0f, 1f);
146    private final ValueAnimator.AnimatorUpdateListener mUpdateListener =
147            new ValueAnimator.AnimatorUpdateListener() {
148                @Override
149                public void onAnimationUpdate(ValueAnimator animation) {
150                    invalidateSelf();
151                }
152            };
153
154    /**
155     * The resources against which this drawable was created. Used to attempt
156     * to inflate animators if applyTheme() doesn't get called.
157     */
158    private Resources mRes;
159
160    private AnimatedVectorDrawableState mAnimatedVectorState;
161
162    /** Whether the animator set has been prepared. */
163    private boolean mHasAnimatorSet;
164
165    private boolean mMutated;
166
167    /** Use a internal AnimatorListener to support callbacks during animation events. */
168    private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null;
169    private AnimatorListener mAnimatorListener = null;
170
171    public AnimatedVectorDrawable() {
172        this(null, null);
173    }
174
175    private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) {
176        mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res);
177        mRes = res;
178    }
179
180    @Override
181    public Drawable mutate() {
182        if (!mMutated && super.mutate() == this) {
183            mAnimatedVectorState = new AnimatedVectorDrawableState(
184                    mAnimatedVectorState, mCallback, mRes);
185            mMutated = true;
186        }
187        return this;
188    }
189
190    /**
191     * @hide
192     */
193    public void clearMutated() {
194        super.clearMutated();
195        if (mAnimatedVectorState.mVectorDrawable != null) {
196            mAnimatedVectorState.mVectorDrawable.clearMutated();
197        }
198        mMutated = false;
199    }
200
201    @Override
202    public ConstantState getConstantState() {
203        mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
204        return mAnimatedVectorState;
205    }
206
207    @Override
208    public int getChangingConfigurations() {
209        return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations();
210    }
211
212    @Override
213    public void draw(Canvas canvas) {
214        mAnimatedVectorState.mVectorDrawable.draw(canvas);
215        if (isStarted()) {
216            invalidateSelf();
217        }
218    }
219
220    @Override
221    protected void onBoundsChange(Rect bounds) {
222        mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
223    }
224
225    @Override
226    protected boolean onStateChange(int[] state) {
227        return mAnimatedVectorState.mVectorDrawable.setState(state);
228    }
229
230    @Override
231    protected boolean onLevelChange(int level) {
232        return mAnimatedVectorState.mVectorDrawable.setLevel(level);
233    }
234
235    @Override
236    public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
237        return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection);
238    }
239
240    @Override
241    public int getAlpha() {
242        return mAnimatedVectorState.mVectorDrawable.getAlpha();
243    }
244
245    @Override
246    public void setAlpha(int alpha) {
247        mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
248    }
249
250    @Override
251    public void setColorFilter(ColorFilter colorFilter) {
252        mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
253    }
254
255    @Override
256    public void setTintList(ColorStateList tint) {
257        mAnimatedVectorState.mVectorDrawable.setTintList(tint);
258    }
259
260    @Override
261    public void setHotspot(float x, float y) {
262        mAnimatedVectorState.mVectorDrawable.setHotspot(x, y);
263    }
264
265    @Override
266    public void setHotspotBounds(int left, int top, int right, int bottom) {
267        mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom);
268    }
269
270    @Override
271    public void setTintMode(PorterDuff.Mode tintMode) {
272        mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
273    }
274
275    @Override
276    public boolean setVisible(boolean visible, boolean restart) {
277        mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
278        return super.setVisible(visible, restart);
279    }
280
281    @Override
282    public boolean isStateful() {
283        return mAnimatedVectorState.mVectorDrawable.isStateful();
284    }
285
286    @Override
287    public int getOpacity() {
288        return mAnimatedVectorState.mVectorDrawable.getOpacity();
289    }
290
291    @Override
292    public int getIntrinsicWidth() {
293        return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
294    }
295
296    @Override
297    public int getIntrinsicHeight() {
298        return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
299    }
300
301    @Override
302    public void getOutline(@NonNull Outline outline) {
303        mAnimatedVectorState.mVectorDrawable.getOutline(outline);
304    }
305
306    /** @hide */
307    @Override
308    public Insets getOpticalInsets() {
309        return mAnimatedVectorState.mVectorDrawable.getOpticalInsets();
310    }
311
312    @Override
313    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
314            throws XmlPullParserException, IOException {
315        final AnimatedVectorDrawableState state = mAnimatedVectorState;
316
317        int eventType = parser.getEventType();
318        float pathErrorScale = 1;
319        while (eventType != XmlPullParser.END_DOCUMENT) {
320            if (eventType == XmlPullParser.START_TAG) {
321                final String tagName = parser.getName();
322                if (ANIMATED_VECTOR.equals(tagName)) {
323                    final TypedArray a = obtainAttributes(res, theme, attrs,
324                            R.styleable.AnimatedVectorDrawable);
325                    int drawableRes = a.getResourceId(
326                            R.styleable.AnimatedVectorDrawable_drawable, 0);
327                    if (drawableRes != 0) {
328                        VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable(
329                                drawableRes, theme).mutate();
330                        vectorDrawable.setAllowCaching(false);
331                        vectorDrawable.setCallback(mCallback);
332                        pathErrorScale = vectorDrawable.getPixelSize();
333                        if (state.mVectorDrawable != null) {
334                            state.mVectorDrawable.setCallback(null);
335                        }
336                        state.mVectorDrawable = vectorDrawable;
337                    }
338                    a.recycle();
339                } else if (TARGET.equals(tagName)) {
340                    final TypedArray a = obtainAttributes(res, theme, attrs,
341                            R.styleable.AnimatedVectorDrawableTarget);
342                    final String target = a.getString(
343                            R.styleable.AnimatedVectorDrawableTarget_name);
344                    final int animResId = a.getResourceId(
345                            R.styleable.AnimatedVectorDrawableTarget_animation, 0);
346                    if (animResId != 0) {
347                        if (theme != null) {
348                            final Animator objectAnimator = AnimatorInflater.loadAnimator(
349                                    res, theme, animResId, pathErrorScale);
350                            state.addTargetAnimator(target, objectAnimator);
351                        } else {
352                            // The animation may be theme-dependent. As a
353                            // workaround until Animator has full support for
354                            // applyTheme(), postpone loading the animator
355                            // until we have a theme in applyTheme().
356                            state.addPendingAnimator(animResId, pathErrorScale, target);
357
358                        }
359                    }
360                    a.recycle();
361                }
362            }
363
364            eventType = parser.next();
365        }
366
367        // If we don't have any pending animations, we don't need to hold a
368        // reference to the resources.
369        mRes = state.mPendingAnims == null ? null : res;
370    }
371
372    @Override
373    public boolean canApplyTheme() {
374        return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme())
375                || super.canApplyTheme();
376    }
377
378    @Override
379    public void applyTheme(Theme t) {
380        super.applyTheme(t);
381
382        final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable;
383        if (vectorDrawable != null && vectorDrawable.canApplyTheme()) {
384            vectorDrawable.applyTheme(t);
385        }
386
387        if (t != null) {
388            mAnimatedVectorState.inflatePendingAnimators(t.getResources(), t);
389        }
390
391        // If we don't have any pending animations, we don't need to hold a
392        // reference to the resources.
393        if (mAnimatedVectorState.mPendingAnims == null) {
394            mRes = null;
395        }
396    }
397
398    private static class AnimatedVectorDrawableState extends ConstantState {
399        int mChangingConfigurations;
400        VectorDrawable mVectorDrawable;
401
402        /** Animators that require a theme before inflation. */
403        ArrayList<PendingAnimator> mPendingAnims;
404
405        /** Fully inflated animators awaiting cloning into an AnimatorSet. */
406        ArrayList<Animator> mAnimators;
407
408        /** Map of animators to their target object names */
409        ArrayMap<Animator, String> mTargetNameMap;
410
411        public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy,
412                Callback owner, Resources res) {
413            if (copy != null) {
414                mChangingConfigurations = copy.mChangingConfigurations;
415
416                if (copy.mVectorDrawable != null) {
417                    final ConstantState cs = copy.mVectorDrawable.getConstantState();
418                    if (res != null) {
419                        mVectorDrawable = (VectorDrawable) cs.newDrawable(res);
420                    } else {
421                        mVectorDrawable = (VectorDrawable) cs.newDrawable();
422                    }
423                    mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate();
424                    mVectorDrawable.setCallback(owner);
425                    mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection());
426                    mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
427                    mVectorDrawable.setAllowCaching(false);
428                }
429
430                if (copy.mAnimators != null) {
431                    mAnimators = new ArrayList<>(copy.mAnimators);
432                }
433
434                if (copy.mTargetNameMap != null) {
435                    mTargetNameMap = new ArrayMap<>(copy.mTargetNameMap);
436                }
437
438                if (copy.mPendingAnims != null) {
439                    mPendingAnims = new ArrayList<>(copy.mPendingAnims);
440                }
441            } else {
442                mVectorDrawable = new VectorDrawable();
443            }
444        }
445
446        @Override
447        public boolean canApplyTheme() {
448            return (mVectorDrawable != null && mVectorDrawable.canApplyTheme())
449                    || mPendingAnims != null || super.canApplyTheme();
450        }
451
452        @Override
453        public Drawable newDrawable() {
454            return new AnimatedVectorDrawable(this, null);
455        }
456
457        @Override
458        public Drawable newDrawable(Resources res) {
459            return new AnimatedVectorDrawable(this, res);
460        }
461
462        @Override
463        public int getChangingConfigurations() {
464            return mChangingConfigurations;
465        }
466
467        public void addPendingAnimator(int resId, float pathErrorScale, String target) {
468            if (mPendingAnims == null) {
469                mPendingAnims = new ArrayList<>(1);
470            }
471            mPendingAnims.add(new PendingAnimator(resId, pathErrorScale, target));
472        }
473
474        public void addTargetAnimator(String targetName, Animator animator) {
475            if (mAnimators == null) {
476                mAnimators = new ArrayList<>(1);
477                mTargetNameMap = new ArrayMap<>(1);
478            }
479            mAnimators.add(animator);
480            mTargetNameMap.put(animator, targetName);
481
482            if (DBG_ANIMATION_VECTOR_DRAWABLE) {
483                Log.v(LOGTAG, "add animator  for target " + targetName + " " + animator);
484            }
485        }
486
487        /**
488         * Prepares a local set of mutable animators based on the constant
489         * state.
490         * <p>
491         * If there are any pending uninflated animators, attempts to inflate
492         * them immediately against the provided resources object.
493         *
494         * @param animatorSet the animator set to which the animators should
495         *                    be added
496         * @param res the resources against which to inflate any pending
497         *            animators, or {@code null} if not available
498         */
499        public void prepareLocalAnimators(@NonNull AnimatorSet animatorSet,
500                @Nullable Resources res) {
501            // Check for uninflated animators. We can remove this after we add
502            // support for Animator.applyTheme(). See comments in inflate().
503            if (mPendingAnims != null) {
504                // Attempt to load animators without applying a theme.
505                if (res != null) {
506                    inflatePendingAnimators(res, null);
507                } else {
508                    Log.e(LOGTAG, "Failed to load animators. Either the AnimatedVectorDrawable"
509                            + " must be created using a Resources object or applyTheme() must be"
510                            + " called with a non-null Theme object.");
511                }
512
513                mPendingAnims = null;
514            }
515
516            // Perform a deep copy of the constant state's animators.
517            final int count = mAnimators == null ? 0 : mAnimators.size();
518            if (count > 0) {
519                final Animator firstAnim = prepareLocalAnimator(0);
520                final AnimatorSet.Builder builder = animatorSet.play(firstAnim);
521                for (int i = 1; i < count; ++i) {
522                    final Animator nextAnim = prepareLocalAnimator(i);
523                    builder.with(nextAnim);
524                }
525            }
526        }
527
528        /**
529         * Prepares a local animator for the given index within the constant
530         * state's list of animators.
531         *
532         * @param index the index of the animator within the constant state
533         */
534        private Animator prepareLocalAnimator(int index) {
535            final Animator animator = mAnimators.get(index);
536            final Animator localAnimator = animator.clone();
537            final String targetName = mTargetNameMap.get(animator);
538            final Object target = mVectorDrawable.getTargetByName(targetName);
539            localAnimator.setTarget(target);
540            return localAnimator;
541        }
542
543        /**
544         * Inflates pending animators, if any, against a theme. Clears the list of
545         * pending animators.
546         *
547         * @param t the theme against which to inflate the animators
548         */
549        public void inflatePendingAnimators(@NonNull Resources res, @Nullable Theme t) {
550            final ArrayList<PendingAnimator> pendingAnims = mPendingAnims;
551            if (pendingAnims != null) {
552                mPendingAnims = null;
553
554                for (int i = 0, count = pendingAnims.size(); i < count; i++) {
555                    final PendingAnimator pendingAnimator = pendingAnims.get(i);
556                    final Animator objectAnimator = pendingAnimator.newInstance(res, t);
557                    addTargetAnimator(pendingAnimator.target, objectAnimator);
558                }
559            }
560        }
561
562        /**
563         * Basically a constant state for Animators until we actually implement
564         * constant states for Animators.
565         */
566        private static class PendingAnimator {
567            public final int animResId;
568            public final float pathErrorScale;
569            public final String target;
570
571            public PendingAnimator(int animResId, float pathErrorScale, String target) {
572                this.animResId = animResId;
573                this.pathErrorScale = pathErrorScale;
574                this.target = target;
575            }
576
577            public Animator newInstance(Resources res, Theme theme) {
578                return AnimatorInflater.loadAnimator(res, theme, animResId, pathErrorScale);
579            }
580        }
581    }
582
583    @Override
584    public boolean isRunning() {
585        return mAnimatorSet.isRunning();
586    }
587
588    private boolean isStarted() {
589        return mAnimatorSet.isStarted();
590    }
591
592    /**
593     * Resets the AnimatedVectorDrawable to the start state as specified in the animators.
594     */
595    public void reset() {
596        // TODO: Use reverse or seek to implement reset, when AnimatorSet supports them.
597        start();
598        mAnimatorSet.cancel();
599    }
600
601    @Override
602    public void start() {
603        ensureAnimatorSet();
604
605        // If any one of the animator has not ended, do nothing.
606        if (isStarted()) {
607            return;
608        }
609
610        mAnimatorSet.start();
611        invalidateSelf();
612    }
613
614    @NonNull
615    private void ensureAnimatorSet() {
616        if (!mHasAnimatorSet) {
617            mAnimatedVectorState.prepareLocalAnimators(mAnimatorSet, mRes);
618            mHasAnimatorSet = true;
619            // Setup an infinitely running ValueAnimator, start it when AnimatorSet starts and
620            // end it when AnimatorSet ends, so we get the animation update timing for
621            // invalidating the drawable. Ideally, we would set an update listener on AnimatorSet,
622            // but since AnimatorSet doesn't support that yet, this is the alternative to achieve
623            // the same goal.
624            mUpdateAnim.setRepeatCount(ValueAnimator.INFINITE);
625            mUpdateAnim.addUpdateListener(mUpdateListener);
626            mAnimatorSet.addListener(new AnimatorListener() {
627                @Override
628                public void onAnimationStart(Animator animation) {
629                    mUpdateAnim.start();
630                }
631
632                @Override
633                public void onAnimationEnd(Animator animation) {
634                    mUpdateAnim.end();
635                }
636
637                @Override
638                public void onAnimationCancel(Animator animation) {
639                }
640
641                @Override
642                public void onAnimationRepeat(Animator animation) {
643                }
644            });
645            mRes = null;
646        }
647    }
648
649    @Override
650    public void stop() {
651        mAnimatorSet.end();
652    }
653
654    /**
655     * Reverses ongoing animations or starts pending animations in reverse.
656     * <p>
657     * NOTE: Only works if all animations support reverse. Otherwise, this will
658     * do nothing.
659     * @hide
660     */
661    public void reverse() {
662        ensureAnimatorSet();
663
664        // Only reverse when all the animators can be reversed.
665        if (!canReverse()) {
666            Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()");
667            return;
668        }
669
670        mAnimatorSet.reverse();
671        invalidateSelf();
672    }
673
674    /**
675     * @hide
676     */
677    public boolean canReverse() {
678        return mAnimatorSet.canReverse();
679    }
680
681    private final Callback mCallback = new Callback() {
682        @Override
683        public void invalidateDrawable(Drawable who) {
684            invalidateSelf();
685        }
686
687        @Override
688        public void scheduleDrawable(Drawable who, Runnable what, long when) {
689            scheduleSelf(what, when);
690        }
691
692        @Override
693        public void unscheduleDrawable(Drawable who, Runnable what) {
694            unscheduleSelf(what);
695        }
696    };
697
698    @Override
699    public void registerAnimationCallback(@NonNull AnimationCallback callback) {
700        if (callback == null) {
701            return;
702        }
703
704        // Add listener accordingly.
705        if (mAnimationCallbacks == null) {
706            mAnimationCallbacks = new ArrayList<>();
707        }
708
709        mAnimationCallbacks.add(callback);
710
711        if (mAnimatorListener == null) {
712            // Create a animator listener and trigger the callback events when listener is
713            // triggered.
714            mAnimatorListener = new AnimatorListenerAdapter() {
715                @Override
716                public void onAnimationStart(Animator animation) {
717                    ArrayList<AnimationCallback> tmpCallbacks = new ArrayList<>(mAnimationCallbacks);
718                    int size = tmpCallbacks.size();
719                    for (int i = 0; i < size; i ++) {
720                        tmpCallbacks.get(i).onAnimationStart(AnimatedVectorDrawable.this);
721                    }
722                }
723
724                @Override
725                public void onAnimationEnd(Animator animation) {
726                    ArrayList<AnimationCallback> tmpCallbacks = new ArrayList<>(mAnimationCallbacks);
727                    int size = tmpCallbacks.size();
728                    for (int i = 0; i < size; i ++) {
729                        tmpCallbacks.get(i).onAnimationEnd(AnimatedVectorDrawable.this);
730                    }
731                }
732            };
733        }
734        mAnimatorSet.addListener(mAnimatorListener);
735    }
736
737    // A helper function to clean up the animator listener in the mAnimatorSet.
738    private void removeAnimatorSetListener() {
739        if (mAnimatorListener != null) {
740            mAnimatorSet.removeListener(mAnimatorListener);
741            mAnimatorListener = null;
742        }
743    }
744
745    @Override
746    public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) {
747        if (mAnimationCallbacks == null || callback == null) {
748            // Nothing to be removed.
749            return false;
750        }
751        boolean removed = mAnimationCallbacks.remove(callback);
752
753        //  When the last call back unregistered, remove the listener accordingly.
754        if (mAnimationCallbacks.size() == 0) {
755            removeAnimatorSetListener();
756        }
757        return removed;
758    }
759
760    @Override
761    public void clearAnimationCallbacks() {
762        removeAnimatorSetListener();
763        if (mAnimationCallbacks == null) {
764            return;
765        }
766
767        mAnimationCallbacks.clear();
768    }
769
770}
771