1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.vectordrawable.graphics.drawable;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.Resources;
22 import android.content.res.Resources.Theme;
23 import android.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.graphics.ColorFilter;
26 import android.graphics.PorterDuff;
27 import android.graphics.Rect;
28 import android.graphics.drawable.Animatable;
29 import android.graphics.drawable.Drawable;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.util.Xml;
33 
34 import androidx.annotation.ColorInt;
35 import androidx.annotation.DrawableRes;
36 import androidx.annotation.IntRange;
37 import androidx.collection.SimpleArrayMap;
38 import androidx.core.animation.Animator;
39 import androidx.core.animation.AnimatorInflater;
40 import androidx.core.animation.AnimatorListenerAdapter;
41 import androidx.core.animation.AnimatorSet;
42 import androidx.core.animation.ObjectAnimator;
43 import androidx.core.content.res.TypedArrayUtils;
44 
45 import org.jspecify.annotations.NonNull;
46 import org.jspecify.annotations.Nullable;
47 import org.xmlpull.v1.XmlPullParser;
48 import org.xmlpull.v1.XmlPullParserException;
49 
50 import java.io.IOException;
51 import java.util.ArrayList;
52 
53 /**
54  * This class animates properties of a {@link VectorDrawableCompat} with animations defined using
55  * {@link ObjectAnimator} or {@link AnimatorSet}.
56  *
57  * <p>
58  * SeekableAnimatedVectorDrawable is defined in the same XML format as
59  * {@link android.graphics.drawable.AnimatedVectorDrawable}.
60  * <p>
61  * Here are all the animatable attributes in {@link VectorDrawableCompat}:
62  * <table border="2" align="center" cellpadding="5">
63  *     <thead>
64  *         <tr>
65  *             <th>Element Name</th>
66  *             <th>Animatable attribute name</th>
67  *         </tr>
68  *     </thead>
69  *     <tr>
70  *         <td>&lt;vector&gt;</td>
71  *         <td>alpha</td>
72  *     </tr>
73  *     <tr>
74  *         <td rowspan="7">&lt;group&gt;</td>
75  *         <td>rotation</td>
76  *     </tr>
77  *     <tr>
78  *         <td>pivotX</td>
79  *     </tr>
80  *     <tr>
81  *         <td>pivotY</td>
82  *     </tr>
83  *     <tr>
84  *         <td>scaleX</td>
85  *     </tr>
86  *     <tr>
87  *         <td>scaleY</td>
88  *     </tr>
89  *     <tr>
90  *         <td>translateX</td>
91  *     </tr>
92  *     <tr>
93  *         <td>translateY</td>
94  *     </tr>
95  *     <tr>
96  *         <td rowspan="8">&lt;path&gt;</td>
97  *         <td>fillColor</td>
98  *     </tr>
99  *     <tr>
100  *         <td>pathData</td>
101  *     </tr>
102  *     <tr>
103  *         <td>strokeColor</td>
104  *     </tr>
105  *     <tr>
106  *         <td>strokeWidth</td>
107  *     </tr>
108  *     <tr>
109  *         <td>strokeAlpha</td>
110  *     </tr>
111  *     <tr>
112  *         <td>fillAlpha</td>
113  *     </tr>
114  *     <tr>
115  *         <td>trimPathStart</td>
116  *     </tr>
117  *     <tr>
118  *         <td>trimPathEnd</td>
119  *     </tr>
120  *     <tr>
121  *         <td>trimPathOffset</td>
122  *     </tr>
123  * </table>
124  * <p>
125  * You can always create a SeekableAnimatedVectorDrawable object and use it as a Drawable by the
126  * Java API. In order to refer to SeekableAnimatedVectorDrawable inside an XML file, you can
127  * use app:srcCompat attribute in AppCompat library's ImageButton or ImageView.
128  * <p>
129  * SeekableAnimatedVectorDrawable supports the following features too:
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  * <p>
138  * Unlike {@code AnimatedVectorDrawableCompat}, this class does not delegate to the platform
139  * {@link android.graphics.drawable.AnimatedVectorDrawable} on any API levels.
140  */
141 public class SeekableAnimatedVectorDrawable extends Drawable implements Animatable {
142 
143     private static final String LOGTAG = "SeekableAVD";
144 
145     private static final String ANIMATED_VECTOR = "animated-vector";
146     private static final String TARGET = "target";
147 
148     private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
149 
150     private AnimatedVectorDrawableState mAnimatedVectorState;
151 
152     // An internal listener to bridge between Animator and SAVD's callbacks.
153     private InternalAnimatorListener mAnimatorListener = null;
154 
155     // An array to keep track of multiple callbacks associated with one drawable.
156     @SuppressWarnings("WeakerAccess")
157     ArrayList<AnimationCallback> mAnimationCallbacks = null;
158 
159     /**
160      * Abstract class for animation callback. Used to notify animation events.
161      */
162     public abstract static class AnimationCallback {
163 
164         /**
165          * Called when the animation starts.
166          *
167          * @param drawable The drawable started the animation.
168          */
onAnimationStart(@onNull SeekableAnimatedVectorDrawable drawable)169         public void onAnimationStart(@NonNull SeekableAnimatedVectorDrawable drawable) {
170         }
171 
172         /**
173          * Called when the animation ends.
174          *
175          * @param drawable The drawable finished the animation.
176          */
onAnimationEnd(@onNull SeekableAnimatedVectorDrawable drawable)177         public void onAnimationEnd(@NonNull SeekableAnimatedVectorDrawable drawable) {
178         }
179 
180         /**
181          * Called when the animation is paused.
182          *
183          * @param drawable The drawable.
184          */
onAnimationPause(@onNull SeekableAnimatedVectorDrawable drawable)185         public void onAnimationPause(@NonNull SeekableAnimatedVectorDrawable drawable) {
186         }
187 
188         /**
189          * Called when the animation is resumed.
190          *
191          * @param drawable The drawable.
192          */
onAnimationResume(@onNull SeekableAnimatedVectorDrawable drawable)193         public void onAnimationResume(@NonNull SeekableAnimatedVectorDrawable drawable) {
194         }
195 
196         /**
197          * Called on every frame while the animation is running. The implementation must not
198          * register or unregister any {@link AnimationCallback} here.
199          *
200          * @param drawable The drawable.
201          */
onAnimationUpdate(@onNull SeekableAnimatedVectorDrawable drawable)202         public void onAnimationUpdate(@NonNull SeekableAnimatedVectorDrawable drawable) {
203         }
204     }
205 
206     private final Callback mCallback = new Callback() {
207 
208         @Override
209         public void invalidateDrawable(@NonNull Drawable who) {
210             invalidateSelf();
211         }
212 
213         @Override
214         public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
215             scheduleSelf(what, when);
216         }
217 
218         @Override
219         public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
220             unscheduleSelf(what);
221         }
222     };
223 
SeekableAnimatedVectorDrawable()224     private SeekableAnimatedVectorDrawable() {
225         this(null, null);
226     }
227 
SeekableAnimatedVectorDrawable( @ullable AnimatedVectorDrawableState state, @Nullable Resources res )228     private SeekableAnimatedVectorDrawable(
229             @Nullable AnimatedVectorDrawableState state,
230             @Nullable Resources res
231     ) {
232         if (state != null) {
233             mAnimatedVectorState = state;
234         } else {
235             mAnimatedVectorState =
236                     new AnimatedVectorDrawableState(null, mCallback, res);
237         }
238     }
239 
240     /**
241      * mutate() is not supported. This method simply returns {@code this}.
242      */
243     @Override
mutate()244     public @NonNull Drawable mutate() {
245         return this;
246     }
247 
248     /**
249      * Create a SeekableAnimatedVectorDrawable object.
250      *
251      * @param context the context for creating the animators.
252      * @param resId   the resource ID for SeekableAnimatedVectorDrawable object.
253      * @return a new SeekableAnimatedVectorDrawable or null if parsing error is found.
254      */
create( @onNull Context context, @DrawableRes int resId )255     public static @Nullable SeekableAnimatedVectorDrawable create(
256             @NonNull Context context,
257             @DrawableRes int resId
258     ) {
259         Resources resources = context.getResources();
260         try {
261             //noinspection AndroidLintResourceType - Parse drawable as XML.
262             final XmlPullParser parser = resources.getXml(resId);
263             final AttributeSet attrs = Xml.asAttributeSet(parser);
264             int type;
265             do {
266                 type = parser.next();
267             } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
268             if (type != XmlPullParser.START_TAG) {
269                 throw new XmlPullParserException("No start tag found");
270             }
271             return createFromXmlInner(context.getResources(), parser, attrs, context.getTheme());
272         } catch (XmlPullParserException e) {
273             Log.e(LOGTAG, "parser error", e);
274         } catch (IOException e) {
275             Log.e(LOGTAG, "parser error", e);
276         }
277         return null;
278     }
279 
280     /**
281      * Create a SeekableAnimatedVectorDrawable from inside an XML document using an optional
282      * {@link Theme}. Called on a parser positioned at a tag in an XML
283      * document, tries to create a Drawable from that tag. Returns {@code null}
284      * if the tag is not a valid drawable.
285      */
createFromXmlInner( @onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme )286     public static @NonNull SeekableAnimatedVectorDrawable createFromXmlInner(
287             @NonNull Resources r,
288             @NonNull XmlPullParser parser,
289             @NonNull AttributeSet attrs,
290             @Nullable Theme theme
291     ) throws XmlPullParserException, IOException {
292         final SeekableAnimatedVectorDrawable drawable = new SeekableAnimatedVectorDrawable();
293         drawable.inflate(r, parser, attrs, theme);
294         return drawable;
295     }
296 
297     @Override
getConstantState()298     public @Nullable ConstantState getConstantState() {
299         // We can't support constant state in older platform.
300         // We need Context to create the animator, and we can't save the context in the constant
301         // state.
302         return null;
303     }
304 
305     @Override
getChangingConfigurations()306     public int getChangingConfigurations() {
307         return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations;
308     }
309 
310     @Override
draw(@onNull Canvas canvas)311     public void draw(@NonNull Canvas canvas) {
312         mAnimatedVectorState.mVectorDrawable.draw(canvas);
313         if (mAnimatedVectorState.mAnimatorSet.isStarted()) {
314             invalidateSelf();
315         }
316     }
317 
318     @Override
onBoundsChange(@onNull Rect bounds)319     protected void onBoundsChange(@NonNull Rect bounds) {
320         mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
321     }
322 
323     @Override
onStateChange(int @NonNull [] state)324     protected boolean onStateChange(int @NonNull [] state) {
325         return mAnimatedVectorState.mVectorDrawable.setState(state);
326     }
327 
328     @Override
onLevelChange(int level)329     protected boolean onLevelChange(int level) {
330         return mAnimatedVectorState.mVectorDrawable.setLevel(level);
331     }
332 
333     @IntRange(from = 0, to = 255)
334     @Override
getAlpha()335     public int getAlpha() {
336         return mAnimatedVectorState.mVectorDrawable.getAlpha();
337     }
338 
339     @Override
setAlpha(@ntRangefrom = 0, to = 255) int alpha)340     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
341         mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
342     }
343 
344     @Override
setColorFilter(@ullable ColorFilter colorFilter)345     public void setColorFilter(@Nullable ColorFilter colorFilter) {
346         mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
347     }
348 
349     @Override
getColorFilter()350     public @Nullable ColorFilter getColorFilter() {
351         return mAnimatedVectorState.mVectorDrawable.getColorFilter();
352     }
353 
354     @Override
setTint(@olorInt int tint)355     public void setTint(@ColorInt int tint) {
356         mAnimatedVectorState.mVectorDrawable.setTint(tint);
357     }
358 
359     @Override
setTintList(@ullable ColorStateList tint)360     public void setTintList(@Nullable ColorStateList tint) {
361         mAnimatedVectorState.mVectorDrawable.setTintList(tint);
362     }
363 
364     @Override
setTintMode(PorterDuff.@ullable Mode tintMode)365     public void setTintMode(PorterDuff.@Nullable Mode tintMode) {
366         mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
367     }
368 
369     @Override
setVisible(boolean visible, boolean restart)370     public boolean setVisible(boolean visible, boolean restart) {
371         mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
372         return super.setVisible(visible, restart);
373     }
374 
375     @Override
isStateful()376     public boolean isStateful() {
377         return mAnimatedVectorState.mVectorDrawable.isStateful();
378     }
379 
380     /**
381      * @return The opacity class of the Drawable.
382      * @deprecated This method is no longer used in graphics optimizations
383      */
384     @Deprecated
385     @Override
getOpacity()386     public int getOpacity() {
387         return mAnimatedVectorState.mVectorDrawable.getOpacity();
388     }
389 
390     @Override
getIntrinsicWidth()391     public int getIntrinsicWidth() {
392         return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
393     }
394 
395     @Override
getIntrinsicHeight()396     public int getIntrinsicHeight() {
397         return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
398     }
399 
400     @Override
isAutoMirrored()401     public boolean isAutoMirrored() {
402         return mAnimatedVectorState.mVectorDrawable.isAutoMirrored();
403     }
404 
405     @Override
setAutoMirrored(boolean mirrored)406     public void setAutoMirrored(boolean mirrored) {
407         mAnimatedVectorState.mVectorDrawable.setAutoMirrored(mirrored);
408     }
409 
410     @Override
inflate( @onNull Resources res, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme )411     public void inflate(
412             @NonNull Resources res,
413             @NonNull XmlPullParser parser,
414             @NonNull AttributeSet attrs,
415             @Nullable Theme theme
416     ) throws XmlPullParserException, IOException {
417         int eventType = parser.getEventType();
418         final int innerDepth = parser.getDepth() + 1;
419 
420         // Parse everything until the end of the animated-vector element.
421         while (eventType != XmlPullParser.END_DOCUMENT
422                 && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
423             if (eventType == XmlPullParser.START_TAG) {
424                 final String tagName = parser.getName();
425                 if (DBG_ANIMATION_VECTOR_DRAWABLE) {
426                     Log.v(LOGTAG, "tagName is " + tagName);
427                 }
428                 if (ANIMATED_VECTOR.equals(tagName)) {
429                     final TypedArray a =
430                             TypedArrayUtils.obtainAttributes(res, theme, attrs,
431                                     AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE);
432 
433                     int drawableRes = a.getResourceId(
434                             AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_DRAWABLE, 0);
435                     if (DBG_ANIMATION_VECTOR_DRAWABLE) {
436                         Log.v(LOGTAG, "drawableRes is " + drawableRes);
437                     }
438                     if (drawableRes != 0) {
439                         VectorDrawableCompat vectorDrawable =
440                                 VectorDrawableCompat.createWithoutDelegate(res, drawableRes, theme);
441                         vectorDrawable.setAllowCaching(false);
442                         vectorDrawable.setCallback(mCallback);
443                         if (mAnimatedVectorState.mVectorDrawable != null) {
444                             mAnimatedVectorState.mVectorDrawable.setCallback(null);
445                         }
446                         mAnimatedVectorState.mVectorDrawable = vectorDrawable;
447                     }
448                     a.recycle();
449                 } else if (TARGET.equals(tagName)) {
450                     final TypedArray a =
451                             res.obtainAttributes(attrs,
452                                     AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET);
453                     final String target = a.getString(
454                             AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_NAME);
455 
456                     int id = a.getResourceId(
457                             AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_ANIMATION,
458                             0);
459                     if (id != 0) {
460                         // There are some important features (like path morphing), added into
461                         // Animator code to support AVD at API 21.
462                         Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id);
463                         setupAnimatorsForTarget(target, objectAnimator);
464                     }
465                     a.recycle();
466                 }
467             }
468             eventType = parser.next();
469         }
470 
471         mAnimatedVectorState.setupAnimatorSet();
472     }
473 
474     @Override
inflate( @onNull Resources res, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs )475     public void inflate(
476             @NonNull Resources res,
477             @NonNull XmlPullParser parser,
478             @NonNull AttributeSet attrs
479     ) throws XmlPullParserException, IOException {
480         inflate(res, parser, attrs, null);
481     }
482 
483     @Override
applyTheme(@onNull Theme t)484     public void applyTheme(@NonNull Theme t) {
485         // TODO(b/149342571): support theming in older platform.
486     }
487 
488     @Override
canApplyTheme()489     public boolean canApplyTheme() {
490         // TODO(b/149342571): support theming in older platform.
491         return false;
492     }
493 
494     private static class AnimatedVectorDrawableState extends ConstantState {
495 
496         int mChangingConfigurations;
497         VectorDrawableCompat mVectorDrawable;
498         // Combining the array of Animators into a single AnimatorSet to hook up listener easier.
499         AnimatorSet mAnimatorSet;
500         ArrayList<Animator> mAnimators;
501         SimpleArrayMap<Animator, String> mTargetNameMap;
502 
AnimatedVectorDrawableState( AnimatedVectorDrawableState copy, Callback owner, Resources res )503         AnimatedVectorDrawableState(
504                 AnimatedVectorDrawableState copy,
505                 Callback owner,
506                 Resources res
507         ) {
508             if (copy != null) {
509                 mChangingConfigurations = copy.mChangingConfigurations;
510                 if (copy.mVectorDrawable != null) {
511                     final ConstantState cs = copy.mVectorDrawable.getConstantState();
512                     if (res != null) {
513                         mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(res);
514                     } else {
515                         mVectorDrawable = (VectorDrawableCompat) cs.newDrawable();
516                     }
517                     mVectorDrawable = (VectorDrawableCompat) mVectorDrawable.mutate();
518                     mVectorDrawable.setCallback(owner);
519                     mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
520                     mVectorDrawable.setAllowCaching(false);
521                 }
522                 if (copy.mAnimators != null) {
523                     final int numAnimators = copy.mAnimators.size();
524                     mAnimators = new ArrayList<>(numAnimators);
525                     mTargetNameMap = new SimpleArrayMap<>(numAnimators);
526                     for (int i = 0; i < numAnimators; ++i) {
527                         Animator anim = copy.mAnimators.get(i);
528                         Animator animClone = anim.clone();
529                         String targetName = copy.mTargetNameMap.get(anim);
530                         Object targetObject = mVectorDrawable.getTargetByName(targetName);
531                         animClone.setTarget(targetObject);
532                         mAnimators.add(animClone);
533                         mTargetNameMap.put(animClone, targetName);
534                     }
535                     setupAnimatorSet();
536                 }
537             }
538         }
539 
540         @Override
newDrawable()541         public @NonNull Drawable newDrawable() {
542             throw new IllegalStateException("No constant state support for SDK < 24.");
543         }
544 
545         @Override
newDrawable(Resources res)546         public @NonNull Drawable newDrawable(Resources res) {
547             throw new IllegalStateException("No constant state support for SDK < 24.");
548         }
549 
550         @Override
getChangingConfigurations()551         public int getChangingConfigurations() {
552             return mChangingConfigurations;
553         }
554 
setupAnimatorSet()555         void setupAnimatorSet() {
556             if (mAnimatorSet == null) {
557                 mAnimatorSet = new AnimatorSet();
558             }
559             mAnimatorSet.playTogether(mAnimators);
560         }
561     }
562 
setupAnimatorsForTarget(String name, Animator animator)563     private void setupAnimatorsForTarget(String name, Animator animator) {
564         Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name);
565         animator.setTarget(target);
566         if (mAnimatedVectorState.mAnimators == null) {
567             mAnimatedVectorState.mAnimators = new ArrayList<>();
568             mAnimatedVectorState.mTargetNameMap = new SimpleArrayMap<>();
569         }
570         mAnimatedVectorState.mAnimators.add(animator);
571         mAnimatedVectorState.mTargetNameMap.put(animator, name);
572         if (DBG_ANIMATION_VECTOR_DRAWABLE) {
573             Log.v(LOGTAG, "add animator  for target " + name + " " + animator);
574         }
575     }
576 
577     /**
578      * Returns whether the animation is running (has started and not yet ended).
579      *
580      * @return {@code true} if the animation is running.
581      */
582     @Override
isRunning()583     public boolean isRunning() {
584         return mAnimatedVectorState.mAnimatorSet.isRunning();
585     }
586 
587     /**
588      * Returns whether the animation is currently in a paused state.
589      *
590      * @return {@code true} if the animation is paused.
591      */
isPaused()592     public boolean isPaused() {
593         return mAnimatedVectorState.mAnimatorSet.isPaused();
594     }
595 
596     @Override
start()597     public void start() {
598         // If any one of the animator has not ended, do nothing.
599         if (mAnimatedVectorState.mAnimatorSet.isStarted()) {
600             return;
601         }
602         // Otherwise, kick off animatorSet.
603         mAnimatedVectorState.mAnimatorSet.start();
604         invalidateSelf();
605     }
606 
607     @Override
stop()608     public void stop() {
609         mAnimatedVectorState.mAnimatorSet.end();
610     }
611 
612     /**
613      * Pauses a running animation. This method should only be called on the same thread on which
614      * the animation was started. If the animation has not yet been started or has since ended,
615      * then the call is ignored. Paused animations can be resumed by calling {@link #resume()}.
616      */
pause()617     public void pause() {
618         mAnimatedVectorState.mAnimatorSet.pause();
619     }
620 
621     /**
622      * Resumes a paused animation. The animation resumes from where it left off when it was
623      * paused. This method should only be called on the same thread on which the animation was
624      * started. Calls will be ignored if this {@link SeekableAnimatedVectorDrawable} is not
625      * currently paused.
626      */
resume()627     public void resume() {
628         mAnimatedVectorState.mAnimatorSet.resume();
629     }
630 
631     /**
632      * Sets the position of the animation to the specified point in time. This time should be
633      * between 0 and the total duration of the animation, including any repetition. If the
634      * animation has not yet been started, then it will not advance forward after it is set to this
635      * time; it will simply set the time to this value and perform any appropriate actions based on
636      * that time. If the animation is already running, then setCurrentPlayTime() will set the
637      * current playing time to this value and continue playing from that point.
638      *
639      * @param playTime The time, in milliseconds, to which the animation is advanced or rewound.
640      *                 Unless the animation is reversing, the playtime is considered the time since
641      *                 the end of the start delay of the AnimatorSet in a forward playing direction.
642      */
setCurrentPlayTime(@ntRangefrom = 0) long playTime)643     public void setCurrentPlayTime(@IntRange(from = 0) long playTime) {
644         mAnimatedVectorState.mAnimatorSet.setCurrentPlayTime(playTime);
645         invalidateSelf();
646     }
647 
648     /**
649      * Returns the milliseconds elapsed since the start of the animation.
650      *
651      * <p>For ongoing animations, this method returns the current progress of the animation in
652      * terms of play time. For an animation that has not yet been started: if the animation has been
653      * seeked to a certain time via {@link #setCurrentPlayTime(long)}, the seeked play time will
654      * be returned; otherwise, this method will return 0.
655      *
656      * @return the current position in time of the animation in milliseconds
657      */
658     @IntRange(from = 0)
getCurrentPlayTime()659     public long getCurrentPlayTime() {
660         return mAnimatedVectorState.mAnimatorSet.getCurrentPlayTime();
661     }
662 
663     /**
664      * Gets the total duration of the animation, accounting for animation sequences, start delay,
665      * and repeating. Return {@link Animator#DURATION_INFINITE} if the duration is infinite.
666      *
667      * @return Total time the animation takes to finish, starting from the time {@link #start()}
668      * is called. {@link Animator#DURATION_INFINITE} if the animation or any of the child
669      * animations repeats infinite times.
670      */
getTotalDuration()671     public long getTotalDuration() {
672         return mAnimatedVectorState.mAnimatorSet.getTotalDuration();
673     }
674 
675     class InternalAnimatorListener extends AnimatorListenerAdapter
676             implements Animator.AnimatorUpdateListener {
677 
678         @Override
onAnimationStart(@onNull Animator animation)679         public void onAnimationStart(@NonNull Animator animation) {
680             final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
681             if (callbacks != null) {
682                 for (int i = 0, size = callbacks.size(); i < size; i++) {
683                     callbacks.get(i).onAnimationStart(SeekableAnimatedVectorDrawable.this);
684                 }
685             }
686         }
687 
688         @Override
onAnimationEnd(@onNull Animator animation)689         public void onAnimationEnd(@NonNull Animator animation) {
690             final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
691             if (callbacks != null) {
692                 for (int i = 0, size = callbacks.size(); i < size; i++) {
693                     callbacks.get(i).onAnimationEnd(SeekableAnimatedVectorDrawable.this);
694                 }
695             }
696         }
697 
698         @Override
onAnimationPause(@onNull Animator animation)699         public void onAnimationPause(@NonNull Animator animation) {
700             final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
701             if (callbacks != null) {
702                 for (int i = 0, size = callbacks.size(); i < size; i++) {
703                     callbacks.get(i).onAnimationPause(SeekableAnimatedVectorDrawable.this);
704                 }
705             }
706         }
707 
708         @Override
onAnimationResume(@onNull Animator animation)709         public void onAnimationResume(@NonNull Animator animation) {
710             final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
711             if (callbacks != null) {
712                 for (int i = 0, size = callbacks.size(); i < size; i++) {
713                     callbacks.get(i).onAnimationResume(SeekableAnimatedVectorDrawable.this);
714                 }
715             }
716         }
717 
718         @Override
onAnimationUpdate(@onNull Animator animation)719         public void onAnimationUpdate(@NonNull Animator animation) {
720             final ArrayList<AnimationCallback> callbacks = mAnimationCallbacks;
721             if (callbacks != null) {
722                 for (int i = 0, size = callbacks.size(); i < size; i++) {
723                     callbacks.get(i).onAnimationUpdate(SeekableAnimatedVectorDrawable.this);
724                 }
725             }
726         }
727     }
728 
729     /**
730      * Adds a callback to listen to the animation events.
731      *
732      * @param callback Callback to add.
733      */
registerAnimationCallback(@onNull AnimationCallback callback)734     public void registerAnimationCallback(@NonNull AnimationCallback callback) {
735         // Add listener accordingly.
736         if (mAnimationCallbacks == null) {
737             mAnimationCallbacks = new ArrayList<>();
738         } else if (mAnimationCallbacks.contains(callback)) {
739             // If this call back is already in, then don't need to append another copy.
740             return;
741         } else {
742             mAnimationCallbacks = new ArrayList<>(mAnimationCallbacks);
743         }
744 
745         mAnimationCallbacks.add(callback);
746 
747         if (mAnimatorListener == null) {
748             // Create an internal listener in order to bridge events to our callbacks.
749             mAnimatorListener = new InternalAnimatorListener();
750             mAnimatedVectorState.mAnimatorSet.addListener(mAnimatorListener);
751             mAnimatedVectorState.mAnimatorSet.addPauseListener(mAnimatorListener);
752             mAnimatedVectorState.mAnimatorSet.addUpdateListener(mAnimatorListener);
753         }
754     }
755 
756     /**
757      * A helper function to clean up the animator listener in the mAnimatorSet.
758      */
removeAnimatorSetListener()759     private void removeAnimatorSetListener() {
760         if (mAnimatorListener != null) {
761             mAnimatedVectorState.mAnimatorSet.removeListener(mAnimatorListener);
762             mAnimatedVectorState.mAnimatorSet.removePauseListener(mAnimatorListener);
763             mAnimatedVectorState.mAnimatorSet.removeUpdateListener(mAnimatorListener);
764             mAnimatorListener = null;
765         }
766     }
767 
768     /**
769      * Removes the specified animation callback.
770      *
771      * @param callback Callback to remove.
772      * @return {@code false} if callback didn't exist in the call back list, or {@code true} if
773      * callback has been removed successfully.
774      */
unregisterAnimationCallback(@onNull AnimationCallback callback)775     public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) {
776         if (mAnimationCallbacks == null) {
777             // Nothing to be removed.
778             return false;
779         }
780 
781         boolean removed = false;
782         if (mAnimationCallbacks.contains(callback)) {
783             mAnimationCallbacks = new ArrayList<>(mAnimationCallbacks);
784             mAnimationCallbacks.remove(callback);
785             removed = true;
786         }
787 
788         //  When the last call back unregistered, remove the listener accordingly.
789         if (mAnimationCallbacks.isEmpty()) {
790             removeAnimatorSetListener();
791         }
792         return removed;
793     }
794 
795     /**
796      * Removes all existing animation callbacks.
797      */
clearAnimationCallbacks()798     public void clearAnimationCallbacks() {
799         removeAnimatorSetListener();
800         if (mAnimationCallbacks == null) {
801             return;
802         }
803         mAnimationCallbacks.clear();
804     }
805 }
806