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