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