1/*
2 * Copyright (C) 2015 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.support.graphics.drawable;
16
17import android.animation.Animator;
18import android.animation.AnimatorListenerAdapter;
19import android.animation.AnimatorSet;
20import android.animation.ArgbEvaluator;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.content.res.ColorStateList;
24import android.content.res.Resources;
25import android.content.res.Resources.Theme;
26import android.content.res.TypedArray;
27import android.graphics.Canvas;
28import android.graphics.ColorFilter;
29import android.graphics.PorterDuff;
30import android.graphics.Rect;
31import android.graphics.drawable.Animatable;
32import android.graphics.drawable.AnimatedVectorDrawable;
33import android.graphics.drawable.Drawable;
34import android.os.Build;
35import android.support.annotation.DrawableRes;
36import android.support.annotation.NonNull;
37import android.support.annotation.Nullable;
38import android.support.annotation.RequiresApi;
39import android.support.v4.content.res.ResourcesCompat;
40import android.support.v4.content.res.TypedArrayUtils;
41import android.support.v4.graphics.drawable.DrawableCompat;
42import android.support.v4.util.ArrayMap;
43import android.util.AttributeSet;
44import android.util.Log;
45import android.util.Xml;
46
47import org.xmlpull.v1.XmlPullParser;
48import org.xmlpull.v1.XmlPullParserException;
49
50import java.io.IOException;
51import java.util.ArrayList;
52import java.util.List;
53
54/**
55 * For API 24 and above, this class is delegating to the framework's {@link
56 * AnimatedVectorDrawable}.
57 * For older API version, this class uses {@link android.animation.ObjectAnimator} and
58 * {@link android.animation.AnimatorSet} to animate the properties of a
59 * {@link VectorDrawableCompat} to create an animated drawable.
60 * <p/>
61 * AnimatedVectorDrawableCompat are defined in the same XML format as
62 * {@link AnimatedVectorDrawable}.
63 * <p/>
64 * Here are all the animatable attributes in {@link VectorDrawableCompat}:
65 * <table border="2" align="center" cellpadding="5">
66 *     <thead>
67 *         <tr>
68 *             <th>Element Name</th>
69 *             <th>Animatable attribute name</th>
70 *         </tr>
71 *     </thead>
72 *     <tr>
73 *         <td>&lt;vector&gt;</td>
74 *         <td>alpha</td>
75 *     </tr>
76 *     <tr>
77 *         <td rowspan="7">&lt;group&gt;</td>
78 *         <td>rotation</td>
79 *     </tr>
80 *     <tr>
81 *         <td>pivotX</td>
82 *     </tr>
83 *     <tr>
84 *         <td>pivotY</td>
85 *     </tr>
86 *     <tr>
87 *         <td>scaleX</td>
88 *     </tr>
89 *     <tr>
90 *         <td>scaleY</td>
91 *     </tr>
92 *     <tr>
93 *         <td>translateX</td>
94 *     </tr>
95 *     <tr>
96 *         <td>translateY</td>
97 *     </tr>
98 *     <tr>
99 *         <td rowspan="8">&lt;path&gt;</td>
100 *         <td>fillColor</td>
101 *     </tr>
102 *     <tr>
103 *         <td>pathData</td>
104 *     </tr>
105 *     <tr>
106 *         <td>strokeColor</td>
107 *     </tr>
108 *     <tr>
109 *         <td>strokeWidth</td>
110 *     </tr>
111 *     <tr>
112 *         <td>strokeAlpha</td>
113 *     </tr>
114 *     <tr>
115 *         <td>fillAlpha</td>
116 *     </tr>
117 *     <tr>
118 *         <td>trimPathStart</td>
119 *     </tr>
120 *     <tr>
121 *         <td>trimPathOffset</td>
122 *     </tr>
123 * </table>
124 * <p/>
125 * You can always create a AnimatedVectorDrawableCompat object and use it as a Drawable by the Java
126 * API. In order to refer to AnimatedVectorDrawableCompat inside a XML file, you can use
127 * app:srcCompat attribute in AppCompat library's ImageButton or ImageView.
128 * <p/>
129 * Note that the animation in AnimatedVectorDrawableCompat now can support the following features:
130 * <ul>
131 * <li>Path Morphing (PathType evaluator). This is used for morphing one path into another.</li>
132 * <li>Path Interpolation. This is used to defined a flexible interpolator (represented as a path)
133 * instead of the system defined ones like LinearInterpolator.</li>
134 * <li>Animating 2 values in one ObjectAnimator according to one path's X value and Y value. One
135 * usage is moving one object in both X and Y dimensions along an path.</li>
136 * </ul>
137 */
138
139public class AnimatedVectorDrawableCompat extends VectorDrawableCommon
140        implements Animatable2Compat {
141    private static final String LOGTAG = "AnimatedVDCompat";
142
143    private static final String ANIMATED_VECTOR = "animated-vector";
144    private static final String TARGET = "target";
145
146    private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
147
148    private AnimatedVectorDrawableCompatState mAnimatedVectorState;
149
150    private Context mContext;
151
152    private ArgbEvaluator mArgbEvaluator = null;
153
154    AnimatedVectorDrawableDelegateState mCachedConstantStateDelegate;
155
156    // Use internal listener to support AVDC's callback.
157    private Animator.AnimatorListener mAnimatorListener = null;
158
159    // Use an array to keep track of multiple call back associated with one drawable.
160    private ArrayList<Animatable2Compat.AnimationCallback> mAnimationCallbacks = null;
161
162
163    AnimatedVectorDrawableCompat() {
164        this(null, null, null);
165    }
166
167    private AnimatedVectorDrawableCompat(@Nullable Context context) {
168        this(context, null, null);
169    }
170
171    private AnimatedVectorDrawableCompat(@Nullable Context context,
172            @Nullable AnimatedVectorDrawableCompatState state,
173            @Nullable Resources res) {
174        mContext = context;
175        if (state != null) {
176            mAnimatedVectorState = state;
177        } else {
178            mAnimatedVectorState = new AnimatedVectorDrawableCompatState(context, state, mCallback,
179                    res);
180        }
181    }
182
183    /**
184     * mutate() will be effective only if the getConstantState() is returning non-null.
185     * Otherwise, it just return the current object without modification.
186     */
187    @Override
188    public Drawable mutate() {
189        if (mDelegateDrawable != null) {
190            mDelegateDrawable.mutate();
191        }
192        // For older platforms that there is no delegated drawable, we just return this without
193        // any modification here, and the getConstantState() will return null in this case.
194        return this;
195    }
196
197
198    /**
199     * Create a AnimatedVectorDrawableCompat object.
200     *
201     * @param context the context for creating the animators.
202     * @param resId   the resource ID for AnimatedVectorDrawableCompat object.
203     * @return a new AnimatedVectorDrawableCompat or null if parsing error is found.
204     */
205    @Nullable
206    public static AnimatedVectorDrawableCompat create(@NonNull Context context,
207            @DrawableRes int resId) {
208        if (Build.VERSION.SDK_INT >= 24) {
209            final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context);
210            drawable.mDelegateDrawable = ResourcesCompat.getDrawable(context.getResources(), resId,
211                    context.getTheme());
212            drawable.mDelegateDrawable.setCallback(drawable.mCallback);
213            drawable.mCachedConstantStateDelegate = new AnimatedVectorDrawableDelegateState(
214                    drawable.mDelegateDrawable.getConstantState());
215            return drawable;
216        }
217        Resources resources = context.getResources();
218        try {
219            //noinspection AndroidLintResourceType - Parse drawable as XML.
220            final XmlPullParser parser = resources.getXml(resId);
221            final AttributeSet attrs = Xml.asAttributeSet(parser);
222            int type;
223            while ((type = parser.next()) != XmlPullParser.START_TAG
224                    && type != XmlPullParser.END_DOCUMENT) {
225                // Empty loop
226            }
227            if (type != XmlPullParser.START_TAG) {
228                throw new XmlPullParserException("No start tag found");
229            }
230            return createFromXmlInner(context, context.getResources(), parser, attrs,
231                    context.getTheme());
232        } catch (XmlPullParserException e) {
233            Log.e(LOGTAG, "parser error", e);
234        } catch (IOException e) {
235            Log.e(LOGTAG, "parser error", e);
236        }
237        return null;
238    }
239
240    /**
241     * Create a AnimatedVectorDrawableCompat from inside an XML document using an optional
242     * {@link Theme}. Called on a parser positioned at a tag in an XML
243     * document, tries to create a Drawable from that tag. Returns {@code null}
244     * if the tag is not a valid drawable.
245     */
246    public static AnimatedVectorDrawableCompat createFromXmlInner(Context context, Resources r,
247            XmlPullParser parser, AttributeSet attrs, Theme theme)
248            throws XmlPullParserException, IOException {
249        final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context);
250        drawable.inflate(r, parser, attrs, theme);
251        return drawable;
252    }
253
254    /**
255     * {@inheritDoc}
256     * <strong>Note</strong> that we don't support constant state when SDK < 24.
257     * Make sure you check the return value before using it.
258     */
259    @Override
260    public ConstantState getConstantState() {
261        if (mDelegateDrawable != null && Build.VERSION.SDK_INT >= 24) {
262            return new AnimatedVectorDrawableDelegateState(mDelegateDrawable.getConstantState());
263        }
264        // We can't support constant state in older platform.
265        // We need Context to create the animator, and we can't save the context in the constant
266        // state.
267        return null;
268    }
269
270    @Override
271    public int getChangingConfigurations() {
272        if (mDelegateDrawable != null) {
273            return mDelegateDrawable.getChangingConfigurations();
274        }
275        return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations;
276    }
277
278    @Override
279    public void draw(Canvas canvas) {
280        if (mDelegateDrawable != null) {
281            mDelegateDrawable.draw(canvas);
282            return;
283        }
284        mAnimatedVectorState.mVectorDrawable.draw(canvas);
285        if (mAnimatedVectorState.mAnimatorSet.isStarted()) {
286            invalidateSelf();
287        }
288    }
289
290    @Override
291    protected void onBoundsChange(Rect bounds) {
292        if (mDelegateDrawable != null) {
293            mDelegateDrawable.setBounds(bounds);
294            return;
295        }
296        mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
297    }
298
299    @Override
300    protected boolean onStateChange(int[] state) {
301        if (mDelegateDrawable != null) {
302            return mDelegateDrawable.setState(state);
303        }
304        return mAnimatedVectorState.mVectorDrawable.setState(state);
305    }
306
307    @Override
308    protected boolean onLevelChange(int level) {
309        if (mDelegateDrawable != null) {
310            return mDelegateDrawable.setLevel(level);
311        }
312        return mAnimatedVectorState.mVectorDrawable.setLevel(level);
313    }
314
315    @Override
316    public int getAlpha() {
317        if (mDelegateDrawable != null) {
318            return DrawableCompat.getAlpha(mDelegateDrawable);
319        }
320        return mAnimatedVectorState.mVectorDrawable.getAlpha();
321    }
322
323    @Override
324    public void setAlpha(int alpha) {
325        if (mDelegateDrawable != null) {
326            mDelegateDrawable.setAlpha(alpha);
327            return;
328        }
329        mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
330    }
331
332    @Override
333    public void setColorFilter(ColorFilter colorFilter) {
334        if (mDelegateDrawable != null) {
335            mDelegateDrawable.setColorFilter(colorFilter);
336            return;
337        }
338        mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
339    }
340
341    @Override
342    public void setTint(int tint) {
343        if (mDelegateDrawable != null) {
344            DrawableCompat.setTint(mDelegateDrawable, tint);
345            return;
346        }
347
348        mAnimatedVectorState.mVectorDrawable.setTint(tint);
349    }
350
351    @Override
352    public void setTintList(ColorStateList tint) {
353        if (mDelegateDrawable != null) {
354            DrawableCompat.setTintList(mDelegateDrawable, tint);
355            return;
356        }
357
358        mAnimatedVectorState.mVectorDrawable.setTintList(tint);
359    }
360
361    @Override
362    public void setTintMode(PorterDuff.Mode tintMode) {
363        if (mDelegateDrawable != null) {
364            DrawableCompat.setTintMode(mDelegateDrawable, tintMode);
365            return;
366        }
367
368        mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
369    }
370
371    @Override
372    public boolean setVisible(boolean visible, boolean restart) {
373        if (mDelegateDrawable != null) {
374            return mDelegateDrawable.setVisible(visible, restart);
375        }
376        mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
377        return super.setVisible(visible, restart);
378    }
379
380    @Override
381    public boolean isStateful() {
382        if (mDelegateDrawable != null) {
383            return mDelegateDrawable.isStateful();
384        }
385        return mAnimatedVectorState.mVectorDrawable.isStateful();
386    }
387
388    @Override
389    public int getOpacity() {
390        if (mDelegateDrawable != null) {
391            return mDelegateDrawable.getOpacity();
392        }
393        return mAnimatedVectorState.mVectorDrawable.getOpacity();
394    }
395
396    @Override
397    public int getIntrinsicWidth() {
398        if (mDelegateDrawable != null) {
399            return mDelegateDrawable.getIntrinsicWidth();
400        }
401        return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
402    }
403
404    @Override
405    public int getIntrinsicHeight() {
406        if (mDelegateDrawable != null) {
407            return mDelegateDrawable.getIntrinsicHeight();
408        }
409        return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
410    }
411
412    @Override
413    public boolean isAutoMirrored() {
414        if (mDelegateDrawable != null) {
415            return DrawableCompat.isAutoMirrored(mDelegateDrawable);
416        }
417        return mAnimatedVectorState.mVectorDrawable.isAutoMirrored();
418    }
419
420    @Override
421    public void setAutoMirrored(boolean mirrored) {
422        if (mDelegateDrawable != null) {
423            DrawableCompat.setAutoMirrored(mDelegateDrawable, mirrored);
424            return;
425        }
426        mAnimatedVectorState.mVectorDrawable.setAutoMirrored(mirrored);
427    }
428
429    @Override
430    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
431            throws XmlPullParserException, IOException {
432        if (mDelegateDrawable != null) {
433            DrawableCompat.inflate(mDelegateDrawable, res, parser, attrs, theme);
434            return;
435        }
436        int eventType = parser.getEventType();
437        final int innerDepth = parser.getDepth() + 1;
438
439        // Parse everything until the end of the animated-vector element.
440        while (eventType != XmlPullParser.END_DOCUMENT
441                && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
442            if (eventType == XmlPullParser.START_TAG) {
443                final String tagName = parser.getName();
444                if (DBG_ANIMATION_VECTOR_DRAWABLE) {
445                    Log.v(LOGTAG, "tagName is " + tagName);
446                }
447                if (ANIMATED_VECTOR.equals(tagName)) {
448                    final TypedArray a =
449                            TypedArrayUtils.obtainAttributes(res, theme, attrs,
450                                    AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE);
451
452                    int drawableRes = a.getResourceId(
453                            AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_DRAWABLE, 0);
454                    if (DBG_ANIMATION_VECTOR_DRAWABLE) {
455                        Log.v(LOGTAG, "drawableRes is " + drawableRes);
456                    }
457                    if (drawableRes != 0) {
458                        VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(res,
459                                drawableRes, theme);
460                        vectorDrawable.setAllowCaching(false);
461                        vectorDrawable.setCallback(mCallback);
462                        if (mAnimatedVectorState.mVectorDrawable != null) {
463                            mAnimatedVectorState.mVectorDrawable.setCallback(null);
464                        }
465                        mAnimatedVectorState.mVectorDrawable = vectorDrawable;
466                    }
467                    a.recycle();
468                } else if (TARGET.equals(tagName)) {
469                    final TypedArray a =
470                            res.obtainAttributes(attrs,
471                                    AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET);
472                    final String target = a.getString(
473                            AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_NAME);
474
475                    int id = a.getResourceId(
476                            AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_ANIMATION,
477                            0);
478                    if (id != 0) {
479                        if (mContext != null) {
480                            // There are some important features (like path morphing), added into
481                            // Animator code to support AVD at API 21.
482                            Animator objectAnimator = AnimatorInflaterCompat.loadAnimator(
483                                    mContext, id);
484                            setupAnimatorsForTarget(target, objectAnimator);
485                        } else {
486                            a.recycle();
487                            throw new IllegalStateException("Context can't be null when inflating" +
488                                    " animators");
489                        }
490                    }
491                    a.recycle();
492                }
493            }
494            eventType = parser.next();
495        }
496
497        mAnimatedVectorState.setupAnimatorSet();
498    }
499
500    @Override
501    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs)
502            throws XmlPullParserException, IOException {
503        inflate(res, parser, attrs, null);
504    }
505
506    @Override
507    public void applyTheme(Theme t) {
508        if (mDelegateDrawable != null) {
509            DrawableCompat.applyTheme(mDelegateDrawable, t);
510            return;
511        }
512        // TODO: support theming in older platform.
513        return;
514    }
515
516    @Override
517    public boolean canApplyTheme() {
518        if (mDelegateDrawable != null) {
519            return DrawableCompat.canApplyTheme(mDelegateDrawable);
520        }
521        // TODO: support theming in older platform.
522        return false;
523    }
524
525    /**
526     * Constant state for delegating the creating drawable job.
527     * Instead of creating a VectorDrawable, create a VectorDrawableCompat instance which contains
528     * a delegated VectorDrawable instance.
529     */
530    @RequiresApi(24)
531    private static class AnimatedVectorDrawableDelegateState extends ConstantState {
532        private final ConstantState mDelegateState;
533
534        public AnimatedVectorDrawableDelegateState(ConstantState state) {
535            mDelegateState = state;
536        }
537
538        @Override
539        public Drawable newDrawable() {
540            AnimatedVectorDrawableCompat drawableCompat =
541                    new AnimatedVectorDrawableCompat();
542            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable();
543            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
544            return drawableCompat;
545        }
546
547        @Override
548        public Drawable newDrawable(Resources res) {
549            AnimatedVectorDrawableCompat drawableCompat =
550                    new AnimatedVectorDrawableCompat();
551            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res);
552            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
553            return drawableCompat;
554        }
555
556        @Override
557        public Drawable newDrawable(Resources res, Theme theme) {
558            AnimatedVectorDrawableCompat drawableCompat =
559                    new AnimatedVectorDrawableCompat();
560            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res, theme);
561            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
562            return drawableCompat;
563        }
564
565        @Override
566        public boolean canApplyTheme() {
567            return mDelegateState.canApplyTheme();
568        }
569
570        @Override
571        public int getChangingConfigurations() {
572            return mDelegateState.getChangingConfigurations();
573        }
574    }
575
576    private static class AnimatedVectorDrawableCompatState extends ConstantState {
577        int mChangingConfigurations;
578        VectorDrawableCompat mVectorDrawable;
579        // Combining the array of Animators into a single AnimatorSet to hook up listener easier.
580        AnimatorSet mAnimatorSet;
581        private ArrayList<Animator> mAnimators;
582        ArrayMap<Animator, String> mTargetNameMap;
583
584        public AnimatedVectorDrawableCompatState(Context context,
585                AnimatedVectorDrawableCompatState copy, Callback owner, Resources res) {
586            if (copy != null) {
587                mChangingConfigurations = copy.mChangingConfigurations;
588                if (copy.mVectorDrawable != null) {
589                    final ConstantState cs = copy.mVectorDrawable.getConstantState();
590                    if (res != null) {
591                        mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(res);
592                    } else {
593                        mVectorDrawable = (VectorDrawableCompat) cs.newDrawable();
594                    }
595                    mVectorDrawable = (VectorDrawableCompat) mVectorDrawable.mutate();
596                    mVectorDrawable.setCallback(owner);
597                    mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
598                    mVectorDrawable.setAllowCaching(false);
599                }
600                if (copy.mAnimators != null) {
601                    final int numAnimators = copy.mAnimators.size();
602                    mAnimators = new ArrayList<Animator>(numAnimators);
603                    mTargetNameMap = new ArrayMap<Animator, String>(numAnimators);
604                    for (int i = 0; i < numAnimators; ++i) {
605                        Animator anim = copy.mAnimators.get(i);
606                        Animator animClone = anim.clone();
607                        String targetName = copy.mTargetNameMap.get(anim);
608                        Object targetObject = mVectorDrawable.getTargetByName(targetName);
609                        animClone.setTarget(targetObject);
610                        mAnimators.add(animClone);
611                        mTargetNameMap.put(animClone, targetName);
612                    }
613                    setupAnimatorSet();
614                }
615            }
616        }
617
618        @Override
619        public Drawable newDrawable() {
620            throw new IllegalStateException("No constant state support for SDK < 24.");
621        }
622
623        @Override
624        public Drawable newDrawable(Resources res) {
625            throw new IllegalStateException("No constant state support for SDK < 24.");
626        }
627
628        @Override
629        public int getChangingConfigurations() {
630            return mChangingConfigurations;
631        }
632
633        public void setupAnimatorSet() {
634            if (mAnimatorSet == null) {
635                mAnimatorSet = new AnimatorSet();
636            }
637            mAnimatorSet.playTogether(mAnimators);
638        }
639    }
640
641    /**
642     * Utility function to fix color interpolation prior to Lollipop. Without this fix, colors
643     * are evaluated as raw integers instead of as colors, which leads to artifacts during
644     * fillColor animations.
645     */
646    private void setupColorAnimator(Animator animator) {
647        if (animator instanceof AnimatorSet) {
648            List<Animator> childAnimators = ((AnimatorSet) animator).getChildAnimations();
649            if (childAnimators != null) {
650                for (int i = 0; i < childAnimators.size(); ++i) {
651                    setupColorAnimator(childAnimators.get(i));
652                }
653            }
654        }
655        if (animator instanceof ObjectAnimator) {
656            ObjectAnimator objectAnim = (ObjectAnimator) animator;
657            final String propertyName = objectAnim.getPropertyName();
658            if ("fillColor".equals(propertyName) || "strokeColor".equals(propertyName)) {
659                if (mArgbEvaluator == null) {
660                    mArgbEvaluator = new ArgbEvaluator();
661                }
662                objectAnim.setEvaluator(mArgbEvaluator);
663            }
664        }
665    }
666
667    private void setupAnimatorsForTarget(String name, Animator animator) {
668        Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name);
669        animator.setTarget(target);
670        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
671            setupColorAnimator(animator);
672        }
673        if (mAnimatedVectorState.mAnimators == null) {
674            mAnimatedVectorState.mAnimators = new ArrayList<Animator>();
675            mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>();
676        }
677        mAnimatedVectorState.mAnimators.add(animator);
678        mAnimatedVectorState.mTargetNameMap.put(animator, name);
679        if (DBG_ANIMATION_VECTOR_DRAWABLE) {
680            Log.v(LOGTAG, "add animator  for target " + name + " " + animator);
681        }
682    }
683
684    @Override
685    public boolean isRunning() {
686        if (mDelegateDrawable != null) {
687            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
688            return ((AnimatedVectorDrawable) mDelegateDrawable).isRunning();
689        }
690        return mAnimatedVectorState.mAnimatorSet.isRunning();
691    }
692
693    @Override
694    public void start() {
695        if (mDelegateDrawable != null) {
696            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
697            ((AnimatedVectorDrawable) mDelegateDrawable).start();
698            return;
699        }
700        // If any one of the animator has not ended, do nothing.
701        if (mAnimatedVectorState.mAnimatorSet.isStarted()) {
702            return;
703        }
704        // Otherwise, kick off animatorSet.
705        mAnimatedVectorState.mAnimatorSet.start();
706        invalidateSelf();
707    }
708
709    @Override
710    public void stop() {
711        if (mDelegateDrawable != null) {
712            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
713            ((AnimatedVectorDrawable) mDelegateDrawable).stop();
714            return;
715        }
716        mAnimatedVectorState.mAnimatorSet.end();
717    }
718
719    final Callback mCallback = new Callback() {
720        @Override
721        public void invalidateDrawable(Drawable who) {
722            invalidateSelf();
723        }
724
725        @Override
726        public void scheduleDrawable(Drawable who, Runnable what, long when) {
727            scheduleSelf(what, when);
728        }
729
730        @Override
731        public void unscheduleDrawable(Drawable who, Runnable what) {
732            unscheduleSelf(what);
733        }
734    };
735
736    /**
737     * A helper function to unregister the Animatable2Compat callback from the platform's
738     * Animatable2 callback, while keeping the internal array of callback up to date.
739     */
740    @RequiresApi(23)
741    private static boolean unregisterPlatformCallback(AnimatedVectorDrawable dr,
742            Animatable2Compat.AnimationCallback callback) {
743        return dr.unregisterAnimationCallback(callback.getPlatformCallback());
744    }
745
746    @Override
747    public void registerAnimationCallback(@NonNull Animatable2Compat.AnimationCallback
748            callback) {
749        if (mDelegateDrawable != null) {
750            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
751            registerPlatformCallback((AnimatedVectorDrawable) mDelegateDrawable, callback);
752            return;
753        }
754
755        if (callback == null) {
756            return;
757        }
758
759        // Add listener accordingly.
760        if (mAnimationCallbacks == null) {
761            mAnimationCallbacks = new ArrayList<>();
762        }
763
764        if (mAnimationCallbacks.contains(callback)) {
765            // If this call back is already in, then don't need to append another copy.
766            return;
767        }
768
769        mAnimationCallbacks.add(callback);
770
771        if (mAnimatorListener == null) {
772            // Create a animator listener and trigger the callback events when listener is
773            // triggered.
774            mAnimatorListener = new AnimatorListenerAdapter() {
775                @Override
776                public void onAnimationStart(Animator animation) {
777                    ArrayList<Animatable2Compat.AnimationCallback> tmpCallbacks =
778                            new ArrayList<>(mAnimationCallbacks);
779                    int size = tmpCallbacks.size();
780                    for (int i = 0; i < size; i++) {
781                        tmpCallbacks.get(i).onAnimationStart(AnimatedVectorDrawableCompat.this);
782                    }
783                }
784
785                @Override
786                public void onAnimationEnd(Animator animation) {
787                    ArrayList<Animatable2Compat.AnimationCallback> tmpCallbacks =
788                            new ArrayList<>(mAnimationCallbacks);
789                    int size = tmpCallbacks.size();
790                    for (int i = 0; i < size; i++) {
791                        tmpCallbacks.get(i).onAnimationEnd(AnimatedVectorDrawableCompat.this);
792                    }
793                }
794            };
795        }
796        mAnimatedVectorState.mAnimatorSet.addListener(mAnimatorListener);
797    }
798
799    /**
800     * A helper function to register the Animatable2Compat callback on the platform's Animatable2
801     * callback.
802     */
803    @RequiresApi(23)
804    private static void registerPlatformCallback(@NonNull AnimatedVectorDrawable avd,
805            @NonNull final Animatable2Compat.AnimationCallback callback) {
806        avd.registerAnimationCallback(callback.getPlatformCallback());
807    }
808
809    /**
810     * A helper function to clean up the animator listener in the mAnimatorSet.
811     */
812    private void removeAnimatorSetListener() {
813        if (mAnimatorListener != null) {
814            mAnimatedVectorState.mAnimatorSet.removeListener(mAnimatorListener);
815            mAnimatorListener = null;
816        }
817    }
818
819    @Override
820    public boolean unregisterAnimationCallback(
821            @NonNull Animatable2Compat.AnimationCallback callback) {
822        if (mDelegateDrawable != null) {
823            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
824            unregisterPlatformCallback((AnimatedVectorDrawable) mDelegateDrawable, callback);
825        }
826
827        if (mAnimationCallbacks == null || callback == null) {
828            // Nothing to be removed.
829            return false;
830        }
831        boolean removed = mAnimationCallbacks.remove(callback);
832
833        //  When the last call back unregistered, remove the listener accordingly.
834        if (mAnimationCallbacks.size() == 0) {
835            removeAnimatorSetListener();
836        }
837        return removed;
838    }
839
840    @Override
841    public void clearAnimationCallbacks() {
842        if (mDelegateDrawable != null) {
843            //noinspection AndroidLintNewApi - Implicit when delegate is non-null.
844            ((AnimatedVectorDrawable) mDelegateDrawable).clearAnimationCallbacks();
845            return;
846        }
847        removeAnimatorSetListener();
848        if (mAnimationCallbacks == null) {
849            return;
850        }
851
852        mAnimationCallbacks.clear();
853    }
854
855    /**
856     * Utility function to register callback to Drawable, when the drawable is created from XML and
857     * referred in Java code, e.g: ImageView.getDrawable().
858     * From API 24 on, the drawable is treated as an AnimatedVectorDrawable.
859     * Otherwise, it is treated as AnimatedVectorDrawableCompat.
860     */
861    public static void registerAnimationCallback(Drawable dr,
862            Animatable2Compat.AnimationCallback callback) {
863        if (dr == null || callback == null) {
864            return;
865        }
866        if (!(dr instanceof Animatable)) {
867            return;
868        }
869
870        if (Build.VERSION.SDK_INT >= 24) {
871            registerPlatformCallback((AnimatedVectorDrawable) dr, callback);
872        } else {
873            ((AnimatedVectorDrawableCompat) dr).registerAnimationCallback(callback);
874        }
875    }
876
877    /**
878     * Utility function to unregister animation callback from Drawable, when the drawable is
879     * created from XML and referred in Java code, e.g: ImageView.getDrawable().
880     * From API 24 on, the drawable is treated as an AnimatedVectorDrawable.
881     * Otherwise, it is treated as AnimatedVectorDrawableCompat.
882     */
883    public static boolean unregisterAnimationCallback(Drawable dr,
884            Animatable2Compat.AnimationCallback callback) {
885        if (dr == null || callback == null) {
886            return false;
887        }
888        if (!(dr instanceof Animatable)) {
889            return false;
890        }
891
892        if (Build.VERSION.SDK_INT >= 24) {
893            return unregisterPlatformCallback((AnimatedVectorDrawable) dr, callback);
894        } else {
895            return ((AnimatedVectorDrawableCompat) dr).unregisterAnimationCallback(callback);
896        }
897    }
898
899    /**
900     * Utility function to clear animation callbacks from Drawable, when the drawable is
901     * created from XML and referred in Java code, e.g: ImageView.getDrawable().
902     * From API 24 on, the drawable is treated as an AnimatedVectorDrawable.
903     * Otherwise, it is treated as AnimatedVectorDrawableCompat.
904     */
905    public static void clearAnimationCallbacks(Drawable dr) {
906        if (dr == null || !(dr instanceof Animatable)) {
907            return;
908        }
909        if (Build.VERSION.SDK_INT >= 24) {
910            ((AnimatedVectorDrawable) dr).clearAnimationCallbacks();
911        } else {
912            ((AnimatedVectorDrawableCompat) dr).clearAnimationCallbacks();
913        }
914
915    }
916}
917