AnimatedVectorDrawable.java revision 841f6e19ba8289c8c0953cbfdbad60590eda3c32
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 145 private final ValueAnimator.AnimatorUpdateListener mUpdateListener = 146 new ValueAnimator.AnimatorUpdateListener() { 147 @Override 148 public void onAnimationUpdate(ValueAnimator animation) { 149 invalidateSelf(); 150 } 151 }; 152 153 /** 154 * The resources against which this drawable was created. Used to attempt 155 * to inflate animators if applyTheme() doesn't get called. 156 */ 157 private Resources mRes; 158 159 private AnimatedVectorDrawableState mAnimatedVectorState; 160 161 /** Whether the animator set has been prepared. */ 162 private boolean mHasAnimatorSet; 163 164 private boolean mMutated; 165 166 /** Use a internal AnimatorListener to support callbacks during animation events. */ 167 private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null; 168 private AnimatorListener mAnimatorListener = null; 169 170 public AnimatedVectorDrawable() { 171 this(null, null); 172 } 173 174 private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) { 175 mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res); 176 mRes = res; 177 } 178 179 @Override 180 public Drawable mutate() { 181 if (!mMutated && super.mutate() == this) { 182 mAnimatedVectorState = new AnimatedVectorDrawableState( 183 mAnimatedVectorState, mCallback, mRes); 184 mMutated = true; 185 } 186 return this; 187 } 188 189 /** 190 * @hide 191 */ 192 public void clearMutated() { 193 super.clearMutated(); 194 if (mAnimatedVectorState.mVectorDrawable != null) { 195 mAnimatedVectorState.mVectorDrawable.clearMutated(); 196 } 197 mMutated = false; 198 } 199 200 @Override 201 public ConstantState getConstantState() { 202 mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations(); 203 return mAnimatedVectorState; 204 } 205 206 @Override 207 public int getChangingConfigurations() { 208 return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations(); 209 } 210 211 @Override 212 public void draw(Canvas canvas) { 213 mAnimatedVectorState.mVectorDrawable.draw(canvas); 214 } 215 216 @Override 217 protected void onBoundsChange(Rect bounds) { 218 mAnimatedVectorState.mVectorDrawable.setBounds(bounds); 219 } 220 221 @Override 222 protected boolean onStateChange(int[] state) { 223 return mAnimatedVectorState.mVectorDrawable.setState(state); 224 } 225 226 @Override 227 protected boolean onLevelChange(int level) { 228 return mAnimatedVectorState.mVectorDrawable.setLevel(level); 229 } 230 231 @Override 232 public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) { 233 return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection); 234 } 235 236 @Override 237 public int getAlpha() { 238 return mAnimatedVectorState.mVectorDrawable.getAlpha(); 239 } 240 241 @Override 242 public void setAlpha(int alpha) { 243 mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); 244 } 245 246 @Override 247 public void setColorFilter(ColorFilter colorFilter) { 248 mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); 249 } 250 251 @Override 252 public void setTintList(ColorStateList tint) { 253 mAnimatedVectorState.mVectorDrawable.setTintList(tint); 254 } 255 256 @Override 257 public void setHotspot(float x, float y) { 258 mAnimatedVectorState.mVectorDrawable.setHotspot(x, y); 259 } 260 261 @Override 262 public void setHotspotBounds(int left, int top, int right, int bottom) { 263 mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom); 264 } 265 266 @Override 267 public void setTintMode(PorterDuff.Mode tintMode) { 268 mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); 269 } 270 271 @Override 272 public boolean setVisible(boolean visible, boolean restart) { 273 mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); 274 return super.setVisible(visible, restart); 275 } 276 277 @Override 278 public boolean isStateful() { 279 return mAnimatedVectorState.mVectorDrawable.isStateful(); 280 } 281 282 @Override 283 public int getOpacity() { 284 return mAnimatedVectorState.mVectorDrawable.getOpacity(); 285 } 286 287 @Override 288 public int getIntrinsicWidth() { 289 return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); 290 } 291 292 @Override 293 public int getIntrinsicHeight() { 294 return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); 295 } 296 297 @Override 298 public void getOutline(@NonNull Outline outline) { 299 mAnimatedVectorState.mVectorDrawable.getOutline(outline); 300 } 301 302 /** @hide */ 303 @Override 304 public Insets getOpticalInsets() { 305 return mAnimatedVectorState.mVectorDrawable.getOpticalInsets(); 306 } 307 308 @Override 309 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 310 throws XmlPullParserException, IOException { 311 final AnimatedVectorDrawableState state = mAnimatedVectorState; 312 313 int eventType = parser.getEventType(); 314 float pathErrorScale = 1; 315 while (eventType != XmlPullParser.END_DOCUMENT) { 316 if (eventType == XmlPullParser.START_TAG) { 317 final String tagName = parser.getName(); 318 if (ANIMATED_VECTOR.equals(tagName)) { 319 final TypedArray a = obtainAttributes(res, theme, attrs, 320 R.styleable.AnimatedVectorDrawable); 321 int drawableRes = a.getResourceId( 322 R.styleable.AnimatedVectorDrawable_drawable, 0); 323 if (drawableRes != 0) { 324 VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable( 325 drawableRes, theme).mutate(); 326 vectorDrawable.setAllowCaching(false); 327 vectorDrawable.setCallback(mCallback); 328 pathErrorScale = vectorDrawable.getPixelSize(); 329 if (state.mVectorDrawable != null) { 330 state.mVectorDrawable.setCallback(null); 331 } 332 state.mVectorDrawable = vectorDrawable; 333 } 334 a.recycle(); 335 } else if (TARGET.equals(tagName)) { 336 final TypedArray a = obtainAttributes(res, theme, attrs, 337 R.styleable.AnimatedVectorDrawableTarget); 338 final String target = a.getString( 339 R.styleable.AnimatedVectorDrawableTarget_name); 340 final int animResId = a.getResourceId( 341 R.styleable.AnimatedVectorDrawableTarget_animation, 0); 342 if (animResId != 0) { 343 if (theme != null) { 344 final Animator objectAnimator = AnimatorInflater.loadAnimator( 345 res, theme, animResId, pathErrorScale); 346 state.addTargetAnimator(target, objectAnimator); 347 } else { 348 // The animation may be theme-dependent. As a 349 // workaround until Animator has full support for 350 // applyTheme(), postpone loading the animator 351 // until we have a theme in applyTheme(). 352 state.addPendingAnimator(animResId, pathErrorScale, target); 353 354 } 355 } 356 a.recycle(); 357 } 358 } 359 360 eventType = parser.next(); 361 } 362 363 // If we don't have any pending animations, we don't need to hold a 364 // reference to the resources. 365 mRes = state.mPendingAnims == null ? null : res; 366 } 367 368 @Override 369 public boolean canApplyTheme() { 370 return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme()) 371 || super.canApplyTheme(); 372 } 373 374 @Override 375 public void applyTheme(Theme t) { 376 super.applyTheme(t); 377 378 final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable; 379 if (vectorDrawable != null && vectorDrawable.canApplyTheme()) { 380 vectorDrawable.applyTheme(t); 381 } 382 383 if (t != null) { 384 mAnimatedVectorState.inflatePendingAnimators(t.getResources(), t); 385 } 386 387 // If we don't have any pending animations, we don't need to hold a 388 // reference to the resources. 389 if (mAnimatedVectorState.mPendingAnims == null) { 390 mRes = null; 391 } 392 } 393 394 private static class AnimatedVectorDrawableState extends ConstantState { 395 int mChangingConfigurations; 396 VectorDrawable mVectorDrawable; 397 398 /** Animators that require a theme before inflation. */ 399 ArrayList<PendingAnimator> mPendingAnims; 400 401 /** Fully inflated animators awaiting cloning into an AnimatorSet. */ 402 ArrayList<Animator> mAnimators; 403 404 /** Map of animators to their target object names */ 405 ArrayMap<Animator, String> mTargetNameMap; 406 407 public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy, 408 Callback owner, Resources res) { 409 if (copy != null) { 410 mChangingConfigurations = copy.mChangingConfigurations; 411 412 if (copy.mVectorDrawable != null) { 413 final ConstantState cs = copy.mVectorDrawable.getConstantState(); 414 if (res != null) { 415 mVectorDrawable = (VectorDrawable) cs.newDrawable(res); 416 } else { 417 mVectorDrawable = (VectorDrawable) cs.newDrawable(); 418 } 419 mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate(); 420 mVectorDrawable.setCallback(owner); 421 mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection()); 422 mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); 423 mVectorDrawable.setAllowCaching(false); 424 } 425 426 if (copy.mAnimators != null) { 427 mAnimators = new ArrayList<>(copy.mAnimators); 428 } 429 430 if (copy.mTargetNameMap != null) { 431 mTargetNameMap = new ArrayMap<>(copy.mTargetNameMap); 432 } 433 434 if (copy.mPendingAnims != null) { 435 mPendingAnims = new ArrayList<>(copy.mPendingAnims); 436 } 437 } else { 438 mVectorDrawable = new VectorDrawable(); 439 } 440 } 441 442 @Override 443 public boolean canApplyTheme() { 444 return (mVectorDrawable != null && mVectorDrawable.canApplyTheme()) 445 || mPendingAnims != null || super.canApplyTheme(); 446 } 447 448 @Override 449 public Drawable newDrawable() { 450 return new AnimatedVectorDrawable(this, null); 451 } 452 453 @Override 454 public Drawable newDrawable(Resources res) { 455 return new AnimatedVectorDrawable(this, res); 456 } 457 458 @Override 459 public int getChangingConfigurations() { 460 return mChangingConfigurations; 461 } 462 463 public void addPendingAnimator(int resId, float pathErrorScale, String target) { 464 if (mPendingAnims == null) { 465 mPendingAnims = new ArrayList<>(1); 466 } 467 mPendingAnims.add(new PendingAnimator(resId, pathErrorScale, target)); 468 } 469 470 public void addTargetAnimator(String targetName, Animator animator) { 471 if (mAnimators == null) { 472 mAnimators = new ArrayList<>(1); 473 mTargetNameMap = new ArrayMap<>(1); 474 } 475 mAnimators.add(animator); 476 mTargetNameMap.put(animator, targetName); 477 478 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 479 Log.v(LOGTAG, "add animator for target " + targetName + " " + animator); 480 } 481 } 482 483 /** 484 * Prepares a local set of mutable animators based on the constant 485 * state. 486 * <p> 487 * If there are any pending uninflated animators, attempts to inflate 488 * them immediately against the provided resources object. 489 * 490 * @param animatorSet the animator set to which the animators should 491 * be added 492 * @param res the resources against which to inflate any pending 493 * animators, or {@code null} if not available 494 */ 495 public void prepareLocalAnimators(@NonNull AnimatorSet animatorSet, 496 @NonNull ValueAnimator.AnimatorUpdateListener updateListener, 497 @Nullable Resources res) { 498 // Check for uninflated animators. We can remove this after we add 499 // support for Animator.applyTheme(). See comments in inflate(). 500 if (mPendingAnims != null) { 501 // Attempt to load animators without applying a theme. 502 if (res != null) { 503 inflatePendingAnimators(res, null); 504 } else { 505 Log.e(LOGTAG, "Failed to load animators. Either the AnimatedVectorDrawable" 506 + " must be created using a Resources object or applyTheme() must be" 507 + " called with a non-null Theme object."); 508 } 509 510 mPendingAnims = null; 511 } 512 513 // Perform a deep copy of the constant state's animators. 514 final int count = mAnimators == null ? 0 : mAnimators.size(); 515 if (count > 0) { 516 final Animator firstAnim = prepareLocalAnimator(0); 517 final AnimatorSet.Builder builder = animatorSet.play(firstAnim); 518 for (int i = 1; i < count; ++i) { 519 final Animator nextAnim = prepareLocalAnimator(i); 520 builder.with(nextAnim); 521 } 522 523 // Setup a value animator to get animation update callbacks. 524 long totalDuration = animatorSet.getTotalDuration(); 525 ValueAnimator updateAnim = ValueAnimator.ofFloat(0f, 1f); 526 if (totalDuration == ValueAnimator.DURATION_INFINITE) { 527 updateAnim.setRepeatCount(ValueAnimator.INFINITE); 528 } else { 529 updateAnim.setDuration(totalDuration); 530 } 531 updateAnim.addUpdateListener(updateListener); 532 builder.with(updateAnim); 533 } 534 } 535 536 /** 537 * Prepares a local animator for the given index within the constant 538 * state's list of animators. 539 * 540 * @param index the index of the animator within the constant state 541 */ 542 private Animator prepareLocalAnimator(int index) { 543 final Animator animator = mAnimators.get(index); 544 final Animator localAnimator = animator.clone(); 545 final String targetName = mTargetNameMap.get(animator); 546 final Object target = mVectorDrawable.getTargetByName(targetName); 547 localAnimator.setTarget(target); 548 return localAnimator; 549 } 550 551 /** 552 * Inflates pending animators, if any, against a theme. Clears the list of 553 * pending animators. 554 * 555 * @param t the theme against which to inflate the animators 556 */ 557 public void inflatePendingAnimators(@NonNull Resources res, @Nullable Theme t) { 558 final ArrayList<PendingAnimator> pendingAnims = mPendingAnims; 559 if (pendingAnims != null) { 560 mPendingAnims = null; 561 562 for (int i = 0, count = pendingAnims.size(); i < count; i++) { 563 final PendingAnimator pendingAnimator = pendingAnims.get(i); 564 final Animator objectAnimator = pendingAnimator.newInstance(res, t); 565 addTargetAnimator(pendingAnimator.target, objectAnimator); 566 } 567 } 568 } 569 570 /** 571 * Basically a constant state for Animators until we actually implement 572 * constant states for Animators. 573 */ 574 private static class PendingAnimator { 575 public final int animResId; 576 public final float pathErrorScale; 577 public final String target; 578 579 public PendingAnimator(int animResId, float pathErrorScale, String target) { 580 this.animResId = animResId; 581 this.pathErrorScale = pathErrorScale; 582 this.target = target; 583 } 584 585 public Animator newInstance(Resources res, Theme theme) { 586 return AnimatorInflater.loadAnimator(res, theme, animResId, pathErrorScale); 587 } 588 } 589 } 590 591 @Override 592 public boolean isRunning() { 593 return mAnimatorSet.isRunning(); 594 } 595 596 private boolean isStarted() { 597 return mAnimatorSet.isStarted(); 598 } 599 600 /** 601 * Resets the AnimatedVectorDrawable to the start state as specified in the animators. 602 */ 603 public void reset() { 604 // TODO: Use reverse or seek to implement reset, when AnimatorSet supports them. 605 start(); 606 mAnimatorSet.cancel(); 607 } 608 609 @Override 610 public void start() { 611 ensureAnimatorSet(); 612 613 // If any one of the animator has not ended, do nothing. 614 if (isStarted()) { 615 return; 616 } 617 618 mAnimatorSet.start(); 619 invalidateSelf(); 620 } 621 622 @NonNull 623 private void ensureAnimatorSet() { 624 if (!mHasAnimatorSet) { 625 mAnimatedVectorState.prepareLocalAnimators(mAnimatorSet, mUpdateListener, mRes); 626 mHasAnimatorSet = true; 627 mRes = null; 628 } 629 } 630 631 @Override 632 public void stop() { 633 mAnimatorSet.end(); 634 } 635 636 /** 637 * Reverses ongoing animations or starts pending animations in reverse. 638 * <p> 639 * NOTE: Only works if all animations support reverse. Otherwise, this will 640 * do nothing. 641 * @hide 642 */ 643 public void reverse() { 644 ensureAnimatorSet(); 645 646 // Only reverse when all the animators can be reversed. 647 if (!canReverse()) { 648 Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()"); 649 return; 650 } 651 652 mAnimatorSet.reverse(); 653 invalidateSelf(); 654 } 655 656 /** 657 * @hide 658 */ 659 public boolean canReverse() { 660 return mAnimatorSet.canReverse(); 661 } 662 663 private final Callback mCallback = new Callback() { 664 @Override 665 public void invalidateDrawable(Drawable who) { 666 invalidateSelf(); 667 } 668 669 @Override 670 public void scheduleDrawable(Drawable who, Runnable what, long when) { 671 scheduleSelf(what, when); 672 } 673 674 @Override 675 public void unscheduleDrawable(Drawable who, Runnable what) { 676 unscheduleSelf(what); 677 } 678 }; 679 680 @Override 681 public void registerAnimationCallback(@NonNull AnimationCallback callback) { 682 if (callback == null) { 683 return; 684 } 685 686 // Add listener accordingly. 687 if (mAnimationCallbacks == null) { 688 mAnimationCallbacks = new ArrayList<>(); 689 } 690 691 mAnimationCallbacks.add(callback); 692 693 if (mAnimatorListener == null) { 694 // Create a animator listener and trigger the callback events when listener is 695 // triggered. 696 mAnimatorListener = new AnimatorListenerAdapter() { 697 @Override 698 public void onAnimationStart(Animator animation) { 699 ArrayList<AnimationCallback> tmpCallbacks = new ArrayList<>(mAnimationCallbacks); 700 int size = tmpCallbacks.size(); 701 for (int i = 0; i < size; i ++) { 702 tmpCallbacks.get(i).onAnimationStart(AnimatedVectorDrawable.this); 703 } 704 } 705 706 @Override 707 public void onAnimationEnd(Animator animation) { 708 ArrayList<AnimationCallback> tmpCallbacks = new ArrayList<>(mAnimationCallbacks); 709 int size = tmpCallbacks.size(); 710 for (int i = 0; i < size; i ++) { 711 tmpCallbacks.get(i).onAnimationEnd(AnimatedVectorDrawable.this); 712 } 713 } 714 }; 715 } 716 mAnimatorSet.addListener(mAnimatorListener); 717 } 718 719 // A helper function to clean up the animator listener in the mAnimatorSet. 720 private void removeAnimatorSetListener() { 721 if (mAnimatorListener != null) { 722 mAnimatorSet.removeListener(mAnimatorListener); 723 mAnimatorListener = null; 724 } 725 } 726 727 @Override 728 public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) { 729 if (mAnimationCallbacks == null || callback == null) { 730 // Nothing to be removed. 731 return false; 732 } 733 boolean removed = mAnimationCallbacks.remove(callback); 734 735 // When the last call back unregistered, remove the listener accordingly. 736 if (mAnimationCallbacks.size() == 0) { 737 removeAnimatorSetListener(); 738 } 739 return removed; 740 } 741 742 @Override 743 public void clearAnimationCallbacks() { 744 removeAnimatorSetListener(); 745 if (mAnimationCallbacks == null) { 746 return; 747 } 748 749 mAnimationCallbacks.clear(); 750 } 751 752}