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