AnimatedVectorDrawableCompat.java revision ac5fe7c617c66850fff75a9fce9979c6e5674b0f
1/* 2 * Copyright (C) 2015 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 androidx.vectordrawable.graphics.drawable; 16 17import android.animation.Animator; 18import android.animation.AnimatorListenerAdapter; 19import android.animation.AnimatorSet; 20import android.animation.ArgbEvaluator; 21import android.animation.ObjectAnimator; 22import android.content.Context; 23import android.content.res.ColorStateList; 24import android.content.res.Resources; 25import android.content.res.Resources.Theme; 26import android.content.res.TypedArray; 27import android.graphics.Canvas; 28import android.graphics.ColorFilter; 29import android.graphics.PorterDuff; 30import android.graphics.Rect; 31import android.graphics.drawable.Animatable; 32import android.graphics.drawable.AnimatedVectorDrawable; 33import android.graphics.drawable.Drawable; 34import android.os.Build; 35import androidx.annotation.DrawableRes; 36import androidx.annotation.NonNull; 37import androidx.annotation.Nullable; 38import androidx.annotation.RequiresApi; 39import androidx.core.content.res.ResourcesCompat; 40import androidx.core.content.res.TypedArrayUtils; 41import androidx.core.graphics.drawable.DrawableCompat; 42import androidx.collection.ArrayMap; 43import android.util.AttributeSet; 44import android.util.Log; 45import android.util.Xml; 46 47import org.xmlpull.v1.XmlPullParser; 48import org.xmlpull.v1.XmlPullParserException; 49 50import java.io.IOException; 51import java.util.ArrayList; 52import java.util.List; 53 54/** 55 * For API 24 and above, this class is delegating to the framework's {@link 56 * AnimatedVectorDrawable}. 57 * For older API version, this class uses {@link android.animation.ObjectAnimator} and 58 * {@link android.animation.AnimatorSet} to animate the properties of a 59 * {@link VectorDrawableCompat} to create an animated drawable. 60 * <p/> 61 * AnimatedVectorDrawableCompat are defined in the same XML format as 62 * {@link AnimatedVectorDrawable}. 63 * <p/> 64 * Here are all the animatable attributes in {@link VectorDrawableCompat}: 65 * <table border="2" align="center" cellpadding="5"> 66 * <thead> 67 * <tr> 68 * <th>Element Name</th> 69 * <th>Animatable attribute name</th> 70 * </tr> 71 * </thead> 72 * <tr> 73 * <td><vector></td> 74 * <td>alpha</td> 75 * </tr> 76 * <tr> 77 * <td rowspan="7"><group></td> 78 * <td>rotation</td> 79 * </tr> 80 * <tr> 81 * <td>pivotX</td> 82 * </tr> 83 * <tr> 84 * <td>pivotY</td> 85 * </tr> 86 * <tr> 87 * <td>scaleX</td> 88 * </tr> 89 * <tr> 90 * <td>scaleY</td> 91 * </tr> 92 * <tr> 93 * <td>translateX</td> 94 * </tr> 95 * <tr> 96 * <td>translateY</td> 97 * </tr> 98 * <tr> 99 * <td rowspan="8"><path></td> 100 * <td>fillColor</td> 101 * </tr> 102 * <tr> 103 * <td>pathData</td> 104 * </tr> 105 * <tr> 106 * <td>strokeColor</td> 107 * </tr> 108 * <tr> 109 * <td>strokeWidth</td> 110 * </tr> 111 * <tr> 112 * <td>strokeAlpha</td> 113 * </tr> 114 * <tr> 115 * <td>fillAlpha</td> 116 * </tr> 117 * <tr> 118 * <td>trimPathStart</td> 119 * </tr> 120 * <tr> 121 * <td>trimPathEnd</td> 122 * </tr> 123 * <tr> 124 * <td>trimPathOffset</td> 125 * </tr> 126 * </table> 127 * <p/> 128 * You can always create a AnimatedVectorDrawableCompat object and use it as a Drawable by the Java 129 * API. In order to refer to AnimatedVectorDrawableCompat inside a XML file, you can use 130 * app:srcCompat attribute in AppCompat library's ImageButton or ImageView. 131 * <p/> 132 * Note that the animation in AnimatedVectorDrawableCompat now can support the following features: 133 * <ul> 134 * <li>Path Morphing (PathType evaluator). This is used for morphing one path into another.</li> 135 * <li>Path Interpolation. This is used to defined a flexible interpolator (represented as a path) 136 * instead of the system defined ones like LinearInterpolator.</li> 137 * <li>Animating 2 values in one ObjectAnimator according to one path's X value and Y value. One 138 * usage is moving one object in both X and Y dimensions along an path.</li> 139 * </ul> 140 */ 141 142public class AnimatedVectorDrawableCompat extends VectorDrawableCommon 143 implements Animatable2Compat { 144 private static final String LOGTAG = "AnimatedVDCompat"; 145 146 private static final String ANIMATED_VECTOR = "animated-vector"; 147 private static final String TARGET = "target"; 148 149 private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false; 150 151 private AnimatedVectorDrawableCompatState mAnimatedVectorState; 152 153 private Context mContext; 154 155 private ArgbEvaluator mArgbEvaluator = null; 156 157 AnimatedVectorDrawableDelegateState mCachedConstantStateDelegate; 158 159 // Use internal listener to support AVDC's callback. 160 private Animator.AnimatorListener mAnimatorListener = null; 161 162 // Use an array to keep track of multiple call back associated with one drawable. 163 private ArrayList<Animatable2Compat.AnimationCallback> mAnimationCallbacks = null; 164 165 166 AnimatedVectorDrawableCompat() { 167 this(null, null, null); 168 } 169 170 private AnimatedVectorDrawableCompat(@Nullable Context context) { 171 this(context, null, null); 172 } 173 174 private AnimatedVectorDrawableCompat(@Nullable Context context, 175 @Nullable AnimatedVectorDrawableCompatState state, 176 @Nullable Resources res) { 177 mContext = context; 178 if (state != null) { 179 mAnimatedVectorState = state; 180 } else { 181 mAnimatedVectorState = new AnimatedVectorDrawableCompatState(context, state, mCallback, 182 res); 183 } 184 } 185 186 /** 187 * mutate() will be effective only if the getConstantState() is returning non-null. 188 * Otherwise, it just return the current object without modification. 189 */ 190 @Override 191 public Drawable mutate() { 192 if (mDelegateDrawable != null) { 193 mDelegateDrawable.mutate(); 194 } 195 // For older platforms that there is no delegated drawable, we just return this without 196 // any modification here, and the getConstantState() will return null in this case. 197 return this; 198 } 199 200 201 /** 202 * Create a AnimatedVectorDrawableCompat object. 203 * 204 * @param context the context for creating the animators. 205 * @param resId the resource ID for AnimatedVectorDrawableCompat object. 206 * @return a new AnimatedVectorDrawableCompat or null if parsing error is found. 207 */ 208 @Nullable 209 public static AnimatedVectorDrawableCompat create(@NonNull Context context, 210 @DrawableRes int resId) { 211 if (Build.VERSION.SDK_INT >= 24) { 212 final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context); 213 drawable.mDelegateDrawable = ResourcesCompat.getDrawable(context.getResources(), resId, 214 context.getTheme()); 215 drawable.mDelegateDrawable.setCallback(drawable.mCallback); 216 drawable.mCachedConstantStateDelegate = new AnimatedVectorDrawableDelegateState( 217 drawable.mDelegateDrawable.getConstantState()); 218 return drawable; 219 } 220 Resources resources = context.getResources(); 221 try { 222 //noinspection AndroidLintResourceType - Parse drawable as XML. 223 final XmlPullParser parser = resources.getXml(resId); 224 final AttributeSet attrs = Xml.asAttributeSet(parser); 225 int type; 226 while ((type = parser.next()) != XmlPullParser.START_TAG 227 && type != XmlPullParser.END_DOCUMENT) { 228 // Empty loop 229 } 230 if (type != XmlPullParser.START_TAG) { 231 throw new XmlPullParserException("No start tag found"); 232 } 233 return createFromXmlInner(context, context.getResources(), parser, attrs, 234 context.getTheme()); 235 } catch (XmlPullParserException e) { 236 Log.e(LOGTAG, "parser error", e); 237 } catch (IOException e) { 238 Log.e(LOGTAG, "parser error", e); 239 } 240 return null; 241 } 242 243 /** 244 * Create a AnimatedVectorDrawableCompat from inside an XML document using an optional 245 * {@link Theme}. Called on a parser positioned at a tag in an XML 246 * document, tries to create a Drawable from that tag. Returns {@code null} 247 * if the tag is not a valid drawable. 248 */ 249 public static AnimatedVectorDrawableCompat createFromXmlInner(Context context, Resources r, 250 XmlPullParser parser, AttributeSet attrs, Theme theme) 251 throws XmlPullParserException, IOException { 252 final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context); 253 drawable.inflate(r, parser, attrs, theme); 254 return drawable; 255 } 256 257 /** 258 * {@inheritDoc} 259 * <strong>Note</strong> that we don't support constant state when SDK < 24. 260 * Make sure you check the return value before using it. 261 */ 262 @Override 263 public ConstantState getConstantState() { 264 if (mDelegateDrawable != null && Build.VERSION.SDK_INT >= 24) { 265 return new AnimatedVectorDrawableDelegateState(mDelegateDrawable.getConstantState()); 266 } 267 // We can't support constant state in older platform. 268 // We need Context to create the animator, and we can't save the context in the constant 269 // state. 270 return null; 271 } 272 273 @Override 274 public int getChangingConfigurations() { 275 if (mDelegateDrawable != null) { 276 return mDelegateDrawable.getChangingConfigurations(); 277 } 278 return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations; 279 } 280 281 @Override 282 public void draw(Canvas canvas) { 283 if (mDelegateDrawable != null) { 284 mDelegateDrawable.draw(canvas); 285 return; 286 } 287 mAnimatedVectorState.mVectorDrawable.draw(canvas); 288 if (mAnimatedVectorState.mAnimatorSet.isStarted()) { 289 invalidateSelf(); 290 } 291 } 292 293 @Override 294 protected void onBoundsChange(Rect bounds) { 295 if (mDelegateDrawable != null) { 296 mDelegateDrawable.setBounds(bounds); 297 return; 298 } 299 mAnimatedVectorState.mVectorDrawable.setBounds(bounds); 300 } 301 302 @Override 303 protected boolean onStateChange(int[] state) { 304 if (mDelegateDrawable != null) { 305 return mDelegateDrawable.setState(state); 306 } 307 return mAnimatedVectorState.mVectorDrawable.setState(state); 308 } 309 310 @Override 311 protected boolean onLevelChange(int level) { 312 if (mDelegateDrawable != null) { 313 return mDelegateDrawable.setLevel(level); 314 } 315 return mAnimatedVectorState.mVectorDrawable.setLevel(level); 316 } 317 318 @Override 319 public int getAlpha() { 320 if (mDelegateDrawable != null) { 321 return DrawableCompat.getAlpha(mDelegateDrawable); 322 } 323 return mAnimatedVectorState.mVectorDrawable.getAlpha(); 324 } 325 326 @Override 327 public void setAlpha(int alpha) { 328 if (mDelegateDrawable != null) { 329 mDelegateDrawable.setAlpha(alpha); 330 return; 331 } 332 mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); 333 } 334 335 @Override 336 public void setColorFilter(ColorFilter colorFilter) { 337 if (mDelegateDrawable != null) { 338 mDelegateDrawable.setColorFilter(colorFilter); 339 return; 340 } 341 mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); 342 } 343 344 @Override 345 public void setTint(int tint) { 346 if (mDelegateDrawable != null) { 347 DrawableCompat.setTint(mDelegateDrawable, tint); 348 return; 349 } 350 351 mAnimatedVectorState.mVectorDrawable.setTint(tint); 352 } 353 354 @Override 355 public void setTintList(ColorStateList tint) { 356 if (mDelegateDrawable != null) { 357 DrawableCompat.setTintList(mDelegateDrawable, tint); 358 return; 359 } 360 361 mAnimatedVectorState.mVectorDrawable.setTintList(tint); 362 } 363 364 @Override 365 public void setTintMode(PorterDuff.Mode tintMode) { 366 if (mDelegateDrawable != null) { 367 DrawableCompat.setTintMode(mDelegateDrawable, tintMode); 368 return; 369 } 370 371 mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); 372 } 373 374 @Override 375 public boolean setVisible(boolean visible, boolean restart) { 376 if (mDelegateDrawable != null) { 377 return mDelegateDrawable.setVisible(visible, restart); 378 } 379 mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); 380 return super.setVisible(visible, restart); 381 } 382 383 @Override 384 public boolean isStateful() { 385 if (mDelegateDrawable != null) { 386 return mDelegateDrawable.isStateful(); 387 } 388 return mAnimatedVectorState.mVectorDrawable.isStateful(); 389 } 390 391 @Override 392 public int getOpacity() { 393 if (mDelegateDrawable != null) { 394 return mDelegateDrawable.getOpacity(); 395 } 396 return mAnimatedVectorState.mVectorDrawable.getOpacity(); 397 } 398 399 @Override 400 public int getIntrinsicWidth() { 401 if (mDelegateDrawable != null) { 402 return mDelegateDrawable.getIntrinsicWidth(); 403 } 404 return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); 405 } 406 407 @Override 408 public int getIntrinsicHeight() { 409 if (mDelegateDrawable != null) { 410 return mDelegateDrawable.getIntrinsicHeight(); 411 } 412 return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); 413 } 414 415 @Override 416 public boolean isAutoMirrored() { 417 if (mDelegateDrawable != null) { 418 return DrawableCompat.isAutoMirrored(mDelegateDrawable); 419 } 420 return mAnimatedVectorState.mVectorDrawable.isAutoMirrored(); 421 } 422 423 @Override 424 public void setAutoMirrored(boolean mirrored) { 425 if (mDelegateDrawable != null) { 426 DrawableCompat.setAutoMirrored(mDelegateDrawable, mirrored); 427 return; 428 } 429 mAnimatedVectorState.mVectorDrawable.setAutoMirrored(mirrored); 430 } 431 432 @Override 433 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 434 throws XmlPullParserException, IOException { 435 if (mDelegateDrawable != null) { 436 DrawableCompat.inflate(mDelegateDrawable, res, parser, attrs, theme); 437 return; 438 } 439 int eventType = parser.getEventType(); 440 final int innerDepth = parser.getDepth() + 1; 441 442 // Parse everything until the end of the animated-vector element. 443 while (eventType != XmlPullParser.END_DOCUMENT 444 && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) { 445 if (eventType == XmlPullParser.START_TAG) { 446 final String tagName = parser.getName(); 447 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 448 Log.v(LOGTAG, "tagName is " + tagName); 449 } 450 if (ANIMATED_VECTOR.equals(tagName)) { 451 final TypedArray a = 452 TypedArrayUtils.obtainAttributes(res, theme, attrs, 453 AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE); 454 455 int drawableRes = a.getResourceId( 456 AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_DRAWABLE, 0); 457 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 458 Log.v(LOGTAG, "drawableRes is " + drawableRes); 459 } 460 if (drawableRes != 0) { 461 VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(res, 462 drawableRes, theme); 463 vectorDrawable.setAllowCaching(false); 464 vectorDrawable.setCallback(mCallback); 465 if (mAnimatedVectorState.mVectorDrawable != null) { 466 mAnimatedVectorState.mVectorDrawable.setCallback(null); 467 } 468 mAnimatedVectorState.mVectorDrawable = vectorDrawable; 469 } 470 a.recycle(); 471 } else if (TARGET.equals(tagName)) { 472 final TypedArray a = 473 res.obtainAttributes(attrs, 474 AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET); 475 final String target = a.getString( 476 AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_NAME); 477 478 int id = a.getResourceId( 479 AndroidResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_ANIMATION, 480 0); 481 if (id != 0) { 482 if (mContext != null) { 483 // There are some important features (like path morphing), added into 484 // Animator code to support AVD at API 21. 485 Animator objectAnimator = AnimatorInflaterCompat.loadAnimator( 486 mContext, id); 487 setupAnimatorsForTarget(target, objectAnimator); 488 } else { 489 a.recycle(); 490 throw new IllegalStateException("Context can't be null when inflating" + 491 " animators"); 492 } 493 } 494 a.recycle(); 495 } 496 } 497 eventType = parser.next(); 498 } 499 500 mAnimatedVectorState.setupAnimatorSet(); 501 } 502 503 @Override 504 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs) 505 throws XmlPullParserException, IOException { 506 inflate(res, parser, attrs, null); 507 } 508 509 @Override 510 public void applyTheme(Theme t) { 511 if (mDelegateDrawable != null) { 512 DrawableCompat.applyTheme(mDelegateDrawable, t); 513 return; 514 } 515 // TODO: support theming in older platform. 516 return; 517 } 518 519 @Override 520 public boolean canApplyTheme() { 521 if (mDelegateDrawable != null) { 522 return DrawableCompat.canApplyTheme(mDelegateDrawable); 523 } 524 // TODO: support theming in older platform. 525 return false; 526 } 527 528 /** 529 * Constant state for delegating the creating drawable job. 530 * Instead of creating a VectorDrawable, create a VectorDrawableCompat instance which contains 531 * a delegated VectorDrawable instance. 532 */ 533 @RequiresApi(24) 534 private static class AnimatedVectorDrawableDelegateState extends ConstantState { 535 private final ConstantState mDelegateState; 536 537 public AnimatedVectorDrawableDelegateState(ConstantState state) { 538 mDelegateState = state; 539 } 540 541 @Override 542 public Drawable newDrawable() { 543 AnimatedVectorDrawableCompat drawableCompat = 544 new AnimatedVectorDrawableCompat(); 545 drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(); 546 drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback); 547 return drawableCompat; 548 } 549 550 @Override 551 public Drawable newDrawable(Resources res) { 552 AnimatedVectorDrawableCompat drawableCompat = 553 new AnimatedVectorDrawableCompat(); 554 drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res); 555 drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback); 556 return drawableCompat; 557 } 558 559 @Override 560 public Drawable newDrawable(Resources res, Theme theme) { 561 AnimatedVectorDrawableCompat drawableCompat = 562 new AnimatedVectorDrawableCompat(); 563 drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res, theme); 564 drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback); 565 return drawableCompat; 566 } 567 568 @Override 569 public boolean canApplyTheme() { 570 return mDelegateState.canApplyTheme(); 571 } 572 573 @Override 574 public int getChangingConfigurations() { 575 return mDelegateState.getChangingConfigurations(); 576 } 577 } 578 579 private static class AnimatedVectorDrawableCompatState extends ConstantState { 580 int mChangingConfigurations; 581 VectorDrawableCompat mVectorDrawable; 582 // Combining the array of Animators into a single AnimatorSet to hook up listener easier. 583 AnimatorSet mAnimatorSet; 584 private ArrayList<Animator> mAnimators; 585 ArrayMap<Animator, String> mTargetNameMap; 586 587 public AnimatedVectorDrawableCompatState(Context context, 588 AnimatedVectorDrawableCompatState copy, Callback owner, Resources res) { 589 if (copy != null) { 590 mChangingConfigurations = copy.mChangingConfigurations; 591 if (copy.mVectorDrawable != null) { 592 final ConstantState cs = copy.mVectorDrawable.getConstantState(); 593 if (res != null) { 594 mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(res); 595 } else { 596 mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(); 597 } 598 mVectorDrawable = (VectorDrawableCompat) mVectorDrawable.mutate(); 599 mVectorDrawable.setCallback(owner); 600 mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); 601 mVectorDrawable.setAllowCaching(false); 602 } 603 if (copy.mAnimators != null) { 604 final int numAnimators = copy.mAnimators.size(); 605 mAnimators = new ArrayList<Animator>(numAnimators); 606 mTargetNameMap = new ArrayMap<Animator, String>(numAnimators); 607 for (int i = 0; i < numAnimators; ++i) { 608 Animator anim = copy.mAnimators.get(i); 609 Animator animClone = anim.clone(); 610 String targetName = copy.mTargetNameMap.get(anim); 611 Object targetObject = mVectorDrawable.getTargetByName(targetName); 612 animClone.setTarget(targetObject); 613 mAnimators.add(animClone); 614 mTargetNameMap.put(animClone, targetName); 615 } 616 setupAnimatorSet(); 617 } 618 } 619 } 620 621 @Override 622 public Drawable newDrawable() { 623 throw new IllegalStateException("No constant state support for SDK < 24."); 624 } 625 626 @Override 627 public Drawable newDrawable(Resources res) { 628 throw new IllegalStateException("No constant state support for SDK < 24."); 629 } 630 631 @Override 632 public int getChangingConfigurations() { 633 return mChangingConfigurations; 634 } 635 636 public void setupAnimatorSet() { 637 if (mAnimatorSet == null) { 638 mAnimatorSet = new AnimatorSet(); 639 } 640 mAnimatorSet.playTogether(mAnimators); 641 } 642 } 643 644 /** 645 * Utility function to fix color interpolation prior to Lollipop. Without this fix, colors 646 * are evaluated as raw integers instead of as colors, which leads to artifacts during 647 * fillColor animations. 648 */ 649 private void setupColorAnimator(Animator animator) { 650 if (animator instanceof AnimatorSet) { 651 List<Animator> childAnimators = ((AnimatorSet) animator).getChildAnimations(); 652 if (childAnimators != null) { 653 for (int i = 0; i < childAnimators.size(); ++i) { 654 setupColorAnimator(childAnimators.get(i)); 655 } 656 } 657 } 658 if (animator instanceof ObjectAnimator) { 659 ObjectAnimator objectAnim = (ObjectAnimator) animator; 660 final String propertyName = objectAnim.getPropertyName(); 661 if ("fillColor".equals(propertyName) || "strokeColor".equals(propertyName)) { 662 if (mArgbEvaluator == null) { 663 mArgbEvaluator = new ArgbEvaluator(); 664 } 665 objectAnim.setEvaluator(mArgbEvaluator); 666 } 667 } 668 } 669 670 private void setupAnimatorsForTarget(String name, Animator animator) { 671 Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name); 672 animator.setTarget(target); 673 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 674 setupColorAnimator(animator); 675 } 676 if (mAnimatedVectorState.mAnimators == null) { 677 mAnimatedVectorState.mAnimators = new ArrayList<Animator>(); 678 mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>(); 679 } 680 mAnimatedVectorState.mAnimators.add(animator); 681 mAnimatedVectorState.mTargetNameMap.put(animator, name); 682 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 683 Log.v(LOGTAG, "add animator for target " + name + " " + animator); 684 } 685 } 686 687 @Override 688 public boolean isRunning() { 689 if (mDelegateDrawable != null) { 690 //noinspection AndroidLintNewApi - Implicit when delegate is non-null. 691 return ((AnimatedVectorDrawable) mDelegateDrawable).isRunning(); 692 } 693 return mAnimatedVectorState.mAnimatorSet.isRunning(); 694 } 695 696 @Override 697 public void start() { 698 if (mDelegateDrawable != null) { 699 //noinspection AndroidLintNewApi - Implicit when delegate is non-null. 700 ((AnimatedVectorDrawable) mDelegateDrawable).start(); 701 return; 702 } 703 // If any one of the animator has not ended, do nothing. 704 if (mAnimatedVectorState.mAnimatorSet.isStarted()) { 705 return; 706 } 707 // Otherwise, kick off animatorSet. 708 mAnimatedVectorState.mAnimatorSet.start(); 709 invalidateSelf(); 710 } 711 712 @Override 713 public void stop() { 714 if (mDelegateDrawable != null) { 715 //noinspection AndroidLintNewApi - Implicit when delegate is non-null. 716 ((AnimatedVectorDrawable) mDelegateDrawable).stop(); 717 return; 718 } 719 mAnimatedVectorState.mAnimatorSet.end(); 720 } 721 722 final Callback mCallback = new Callback() { 723 @Override 724 public void invalidateDrawable(Drawable who) { 725 invalidateSelf(); 726 } 727 728 @Override 729 public void scheduleDrawable(Drawable who, Runnable what, long when) { 730 scheduleSelf(what, when); 731 } 732 733 @Override 734 public void unscheduleDrawable(Drawable who, Runnable what) { 735 unscheduleSelf(what); 736 } 737 }; 738 739 /** 740 * A helper function to unregister the Animatable2Compat callback from the platform's 741 * Animatable2 callback, while keeping the internal array of callback up to date. 742 */ 743 @RequiresApi(23) 744 private static boolean unregisterPlatformCallback(AnimatedVectorDrawable dr, 745 Animatable2Compat.AnimationCallback callback) { 746 return dr.unregisterAnimationCallback(callback.getPlatformCallback()); 747 } 748 749 @Override 750 public void registerAnimationCallback(@NonNull Animatable2Compat.AnimationCallback 751 callback) { 752 if (mDelegateDrawable != null) { 753 //noinspection AndroidLintNewApi - Implicit when delegate is non-null. 754 registerPlatformCallback((AnimatedVectorDrawable) mDelegateDrawable, callback); 755 return; 756 } 757 758 if (callback == null) { 759 return; 760 } 761 762 // Add listener accordingly. 763 if (mAnimationCallbacks == null) { 764 mAnimationCallbacks = new ArrayList<>(); 765 } 766 767 if (mAnimationCallbacks.contains(callback)) { 768 // If this call back is already in, then don't need to append another copy. 769 return; 770 } 771 772 mAnimationCallbacks.add(callback); 773 774 if (mAnimatorListener == null) { 775 // Create a animator listener and trigger the callback events when listener is 776 // triggered. 777 mAnimatorListener = new AnimatorListenerAdapter() { 778 @Override 779 public void onAnimationStart(Animator animation) { 780 ArrayList<Animatable2Compat.AnimationCallback> tmpCallbacks = 781 new ArrayList<>(mAnimationCallbacks); 782 int size = tmpCallbacks.size(); 783 for (int i = 0; i < size; i++) { 784 tmpCallbacks.get(i).onAnimationStart(AnimatedVectorDrawableCompat.this); 785 } 786 } 787 788 @Override 789 public void onAnimationEnd(Animator animation) { 790 ArrayList<Animatable2Compat.AnimationCallback> tmpCallbacks = 791 new ArrayList<>(mAnimationCallbacks); 792 int size = tmpCallbacks.size(); 793 for (int i = 0; i < size; i++) { 794 tmpCallbacks.get(i).onAnimationEnd(AnimatedVectorDrawableCompat.this); 795 } 796 } 797 }; 798 } 799 mAnimatedVectorState.mAnimatorSet.addListener(mAnimatorListener); 800 } 801 802 /** 803 * A helper function to register the Animatable2Compat callback on the platform's Animatable2 804 * callback. 805 */ 806 @RequiresApi(23) 807 private static void registerPlatformCallback(@NonNull AnimatedVectorDrawable avd, 808 @NonNull final Animatable2Compat.AnimationCallback callback) { 809 avd.registerAnimationCallback(callback.getPlatformCallback()); 810 } 811 812 /** 813 * A helper function to clean up the animator listener in the mAnimatorSet. 814 */ 815 private void removeAnimatorSetListener() { 816 if (mAnimatorListener != null) { 817 mAnimatedVectorState.mAnimatorSet.removeListener(mAnimatorListener); 818 mAnimatorListener = null; 819 } 820 } 821 822 @Override 823 public boolean unregisterAnimationCallback( 824 @NonNull Animatable2Compat.AnimationCallback callback) { 825 if (mDelegateDrawable != null) { 826 //noinspection AndroidLintNewApi - Implicit when delegate is non-null. 827 unregisterPlatformCallback((AnimatedVectorDrawable) mDelegateDrawable, callback); 828 } 829 830 if (mAnimationCallbacks == null || callback == null) { 831 // Nothing to be removed. 832 return false; 833 } 834 boolean removed = mAnimationCallbacks.remove(callback); 835 836 // When the last call back unregistered, remove the listener accordingly. 837 if (mAnimationCallbacks.size() == 0) { 838 removeAnimatorSetListener(); 839 } 840 return removed; 841 } 842 843 @Override 844 public void clearAnimationCallbacks() { 845 if (mDelegateDrawable != null) { 846 //noinspection AndroidLintNewApi - Implicit when delegate is non-null. 847 ((AnimatedVectorDrawable) mDelegateDrawable).clearAnimationCallbacks(); 848 return; 849 } 850 removeAnimatorSetListener(); 851 if (mAnimationCallbacks == null) { 852 return; 853 } 854 855 mAnimationCallbacks.clear(); 856 } 857 858 /** 859 * Utility function to register callback to Drawable, when the drawable is created from XML and 860 * referred in Java code, e.g: ImageView.getDrawable(). 861 * From API 24 on, the drawable is treated as an AnimatedVectorDrawable. 862 * Otherwise, it is treated as AnimatedVectorDrawableCompat. 863 */ 864 public static void registerAnimationCallback(Drawable dr, 865 Animatable2Compat.AnimationCallback callback) { 866 if (dr == null || callback == null) { 867 return; 868 } 869 if (!(dr instanceof Animatable)) { 870 return; 871 } 872 873 if (Build.VERSION.SDK_INT >= 24) { 874 registerPlatformCallback((AnimatedVectorDrawable) dr, callback); 875 } else { 876 ((AnimatedVectorDrawableCompat) dr).registerAnimationCallback(callback); 877 } 878 } 879 880 /** 881 * Utility function to unregister animation callback from Drawable, when the drawable is 882 * created from XML and referred in Java code, e.g: ImageView.getDrawable(). 883 * From API 24 on, the drawable is treated as an AnimatedVectorDrawable. 884 * Otherwise, it is treated as AnimatedVectorDrawableCompat. 885 */ 886 public static boolean unregisterAnimationCallback(Drawable dr, 887 Animatable2Compat.AnimationCallback callback) { 888 if (dr == null || callback == null) { 889 return false; 890 } 891 if (!(dr instanceof Animatable)) { 892 return false; 893 } 894 895 if (Build.VERSION.SDK_INT >= 24) { 896 return unregisterPlatformCallback((AnimatedVectorDrawable) dr, callback); 897 } else { 898 return ((AnimatedVectorDrawableCompat) dr).unregisterAnimationCallback(callback); 899 } 900 } 901 902 /** 903 * Utility function to clear animation callbacks from Drawable, when the drawable is 904 * created from XML and referred in Java code, e.g: ImageView.getDrawable(). 905 * From API 24 on, the drawable is treated as an AnimatedVectorDrawable. 906 * Otherwise, it is treated as AnimatedVectorDrawableCompat. 907 */ 908 public static void clearAnimationCallbacks(Drawable dr) { 909 if (dr == null || !(dr instanceof Animatable)) { 910 return; 911 } 912 if (Build.VERSION.SDK_INT >= 24) { 913 ((AnimatedVectorDrawable) dr).clearAnimationCallbacks(); 914 } else { 915 ((AnimatedVectorDrawableCompat) dr).clearAnimationCallbacks(); 916 } 917 918 } 919} 920