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