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><vector></td> 71 * <td>alpha</td> 72 * </tr> 73 * <tr> 74 * <td rowspan="7"><group></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"><path></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