AnimatedVectorDrawable.java revision 9b115a9870a184e32bdbd07f792f9b8c956c75ca
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 /** 139 * The resources against which this drawable was created. Used to attempt 140 * to inflate animators if applyTheme() doesn't get called. 141 */ 142 private Resources mRes; 143 144 private AnimatedVectorDrawableState mAnimatedVectorState; 145 146 private boolean mMutated; 147 148 public AnimatedVectorDrawable() { 149 this(null, null); 150 } 151 152 private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) { 153 mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res); 154 mRes = res; 155 } 156 157 @Override 158 public Drawable mutate() { 159 if (!mMutated && super.mutate() == this) { 160 mAnimatedVectorState = new AnimatedVectorDrawableState( 161 mAnimatedVectorState, mCallback, null); 162 mMutated = true; 163 } 164 return this; 165 } 166 167 /** 168 * @hide 169 */ 170 public void clearMutated() { 171 super.clearMutated(); 172 mAnimatedVectorState.mVectorDrawable.clearMutated(); 173 mMutated = false; 174 } 175 176 @Override 177 public ConstantState getConstantState() { 178 mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations(); 179 return mAnimatedVectorState; 180 } 181 182 @Override 183 public int getChangingConfigurations() { 184 return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations(); 185 } 186 187 @Override 188 public void draw(Canvas canvas) { 189 mAnimatedVectorState.mVectorDrawable.draw(canvas); 190 if (isStarted()) { 191 invalidateSelf(); 192 } 193 } 194 195 @Override 196 protected void onBoundsChange(Rect bounds) { 197 mAnimatedVectorState.mVectorDrawable.setBounds(bounds); 198 } 199 200 @Override 201 protected boolean onStateChange(int[] state) { 202 return mAnimatedVectorState.mVectorDrawable.setState(state); 203 } 204 205 @Override 206 protected boolean onLevelChange(int level) { 207 return mAnimatedVectorState.mVectorDrawable.setLevel(level); 208 } 209 210 @Override 211 public boolean onLayoutDirectionChange(int layoutDirection) { 212 return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection); 213 } 214 215 @Override 216 public int getAlpha() { 217 return mAnimatedVectorState.mVectorDrawable.getAlpha(); 218 } 219 220 @Override 221 public void setAlpha(int alpha) { 222 mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); 223 } 224 225 @Override 226 public void setColorFilter(ColorFilter colorFilter) { 227 mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); 228 } 229 230 @Override 231 public void setTintList(ColorStateList tint) { 232 mAnimatedVectorState.mVectorDrawable.setTintList(tint); 233 } 234 235 @Override 236 public void setHotspot(float x, float y) { 237 mAnimatedVectorState.mVectorDrawable.setHotspot(x, y); 238 } 239 240 @Override 241 public void setHotspotBounds(int left, int top, int right, int bottom) { 242 mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom); 243 } 244 245 @Override 246 public void setTintMode(PorterDuff.Mode tintMode) { 247 mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); 248 } 249 250 @Override 251 public boolean setVisible(boolean visible, boolean restart) { 252 mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); 253 return super.setVisible(visible, restart); 254 } 255 256 @Override 257 public boolean isStateful() { 258 return mAnimatedVectorState.mVectorDrawable.isStateful(); 259 } 260 261 @Override 262 public int getOpacity() { 263 return mAnimatedVectorState.mVectorDrawable.getOpacity(); 264 } 265 266 @Override 267 public int getIntrinsicWidth() { 268 return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); 269 } 270 271 @Override 272 public int getIntrinsicHeight() { 273 return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); 274 } 275 276 @Override 277 public void getOutline(@NonNull Outline outline) { 278 mAnimatedVectorState.mVectorDrawable.getOutline(outline); 279 } 280 281 @Override 282 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 283 throws XmlPullParserException, IOException { 284 final AnimatedVectorDrawableState state = mAnimatedVectorState; 285 286 int eventType = parser.getEventType(); 287 float pathErrorScale = 1; 288 while (eventType != XmlPullParser.END_DOCUMENT) { 289 if (eventType == XmlPullParser.START_TAG) { 290 final String tagName = parser.getName(); 291 if (ANIMATED_VECTOR.equals(tagName)) { 292 final TypedArray a = obtainAttributes(res, theme, attrs, 293 R.styleable.AnimatedVectorDrawable); 294 int drawableRes = a.getResourceId( 295 R.styleable.AnimatedVectorDrawable_drawable, 0); 296 if (drawableRes != 0) { 297 VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable( 298 drawableRes, theme).mutate(); 299 vectorDrawable.setAllowCaching(false); 300 vectorDrawable.setCallback(mCallback); 301 pathErrorScale = vectorDrawable.getPixelSize(); 302 if (state.mVectorDrawable != null) { 303 state.mVectorDrawable.setCallback(null); 304 } 305 state.mVectorDrawable = vectorDrawable; 306 } 307 a.recycle(); 308 } else if (TARGET.equals(tagName)) { 309 final TypedArray a = obtainAttributes(res, theme, attrs, 310 R.styleable.AnimatedVectorDrawableTarget); 311 final String target = a.getString( 312 R.styleable.AnimatedVectorDrawableTarget_name); 313 314 final int animResId = a.getResourceId( 315 R.styleable.AnimatedVectorDrawableTarget_animation, 0); 316 if (animResId != 0) { 317 if (theme != null) { 318 final Animator objectAnimator = AnimatorInflater.loadAnimator( 319 res, theme, animResId, pathErrorScale); 320 setupAnimatorsForTarget(target, objectAnimator); 321 } else { 322 // The animation may be theme-dependent. As a 323 // workaround until Animator has full support for 324 // applyTheme(), postpone loading the animator 325 // until we have a theme in applyTheme(). 326 if (state.mPendingAnims == null) { 327 state.mPendingAnims = new ArrayList<>(1); 328 } 329 state.mPendingAnims.add( 330 new PendingAnimator(animResId, pathErrorScale, target)); 331 332 } 333 } 334 a.recycle(); 335 } 336 } 337 338 eventType = parser.next(); 339 } 340 341 // If we don't have any pending animations, we don't need to hold a 342 // reference to the resources. 343 if (state.mPendingAnims == null) { 344 mRes = null; 345 } 346 347 setupAnimatorSet(); 348 } 349 350 private void setupAnimatorSet() { 351 if (mAnimatedVectorState.mTempAnimators != null) { 352 mAnimatedVectorState.mAnimatorSet.playTogether(mAnimatedVectorState.mTempAnimators); 353 mAnimatedVectorState.mTempAnimators.clear(); 354 mAnimatedVectorState.mTempAnimators = null; 355 } 356 } 357 358 @Override 359 public boolean canApplyTheme() { 360 return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme()) 361 || super.canApplyTheme(); 362 } 363 364 @Override 365 public void applyTheme(Theme t) { 366 super.applyTheme(t); 367 368 final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable; 369 if (vectorDrawable != null && vectorDrawable.canApplyTheme()) { 370 vectorDrawable.applyTheme(t); 371 } 372 373 if (t != null) { 374 inflatePendingAnimators(t.getResources(), t); 375 } 376 } 377 378 /** 379 * Inflates pending animators, if any, against a theme. Clears the list of 380 * pending animators. 381 * 382 * @param t the theme against which to inflate the animators 383 */ 384 private void inflatePendingAnimators(@NonNull Resources res, @Nullable Theme t) { 385 final ArrayList<PendingAnimator> pendingAnims = mAnimatedVectorState.mPendingAnims; 386 if (pendingAnims != null) { 387 mAnimatedVectorState.mPendingAnims = null; 388 389 for (int i = 0, count = pendingAnims.size(); i < count; i++) { 390 final PendingAnimator pendingAnimator = pendingAnims.get(i); 391 final Animator objectAnimator = pendingAnimator.newInstance(res, t); 392 setupAnimatorsForTarget(pendingAnimator.target, objectAnimator); 393 } 394 } 395 } 396 397 /** 398 * Adds a listener to the set of listeners that are sent events through the life of an 399 * animation. 400 * 401 * @param listener the listener to be added to the current set of listeners for this animation. 402 */ 403 public void addListener(AnimatorListener listener) { 404 mAnimatedVectorState.mAnimatorSet.addListener(listener); 405 } 406 407 /** 408 * Removes a listener from the set listening to this animation. 409 * 410 * @param listener the listener to be removed from the current set of listeners for this 411 * animation. 412 */ 413 public void removeListener(AnimatorListener listener) { 414 mAnimatedVectorState.mAnimatorSet.removeListener(listener); 415 } 416 417 /** 418 * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently 419 * listening for events on this <code>AnimatedVectorDrawable</code> object. 420 * 421 * @return List<AnimatorListener> The set of listeners. 422 */ 423 public List<AnimatorListener> getListeners() { 424 return mAnimatedVectorState.mAnimatorSet.getListeners(); 425 } 426 427 private static class AnimatedVectorDrawableState extends ConstantState { 428 int mChangingConfigurations; 429 VectorDrawable mVectorDrawable; 430 // Always have a valid animatorSet to handle all the listeners call. 431 AnimatorSet mAnimatorSet = new AnimatorSet(); 432 // When parsing the XML, we build individual animator and store in this array. At the end, 433 // we add this array into the mAnimatorSet. 434 private ArrayList<Animator> mTempAnimators; 435 ArrayMap<Animator, String> mTargetNameMap; 436 ArrayList<PendingAnimator> mPendingAnims; 437 438 public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy, 439 Callback owner, Resources res) { 440 if (copy != null) { 441 mChangingConfigurations = copy.mChangingConfigurations; 442 if (copy.mVectorDrawable != null) { 443 final ConstantState cs = copy.mVectorDrawable.getConstantState(); 444 if (res != null) { 445 mVectorDrawable = (VectorDrawable) cs.newDrawable(res); 446 } else { 447 mVectorDrawable = (VectorDrawable) cs.newDrawable(); 448 } 449 mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate(); 450 mVectorDrawable.setCallback(owner); 451 mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection()); 452 mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); 453 mVectorDrawable.setAllowCaching(false); 454 } 455 if (copy.mAnimatorSet != null) { 456 final int numAnimators = copy.mTargetNameMap.size(); 457 // Deep copy a animator set, and then setup the target map again. 458 mAnimatorSet = copy.mAnimatorSet.clone(); 459 mTargetNameMap = new ArrayMap<>(numAnimators); 460 // Since the new AnimatorSet is cloned from the old one, the order must be the 461 // same inside the array. 462 ArrayList<Animator> oldAnim = copy.mAnimatorSet.getChildAnimations(); 463 ArrayList<Animator> newAnim = mAnimatorSet.getChildAnimations(); 464 465 for (int i = 0; i < numAnimators; ++i) { 466 // Target name must be the same for new and old 467 String targetName = copy.mTargetNameMap.get(oldAnim.get(i)); 468 469 Object newTargetObject = mVectorDrawable.getTargetByName(targetName); 470 newAnim.get(i).setTarget(newTargetObject); 471 mTargetNameMap.put(newAnim.get(i), targetName); 472 } 473 } 474 475 // Shallow copy since the array is immutable after inflate(). 476 mPendingAnims = copy.mPendingAnims; 477 } else { 478 mVectorDrawable = new VectorDrawable(); 479 } 480 } 481 482 @Override 483 public boolean canApplyTheme() { 484 return (mVectorDrawable != null && mVectorDrawable.canApplyTheme()) 485 || mPendingAnims != null || super.canApplyTheme(); 486 } 487 488 @Override 489 public Drawable newDrawable() { 490 return new AnimatedVectorDrawable(this, null); 491 } 492 493 @Override 494 public Drawable newDrawable(Resources res) { 495 return new AnimatedVectorDrawable(this, res); 496 } 497 498 @Override 499 public int getChangingConfigurations() { 500 return mChangingConfigurations; 501 } 502 } 503 504 /** 505 * Basically a constant state for Animators until we actually implement 506 * constant states for Animators. 507 */ 508 private static class PendingAnimator { 509 public final int animResId; 510 public final float pathErrorScale; 511 public final String target; 512 513 public PendingAnimator(int animResId, float pathErrorScale, String target) { 514 this.animResId = animResId; 515 this.pathErrorScale = pathErrorScale; 516 this.target = target; 517 } 518 519 public Animator newInstance(Resources res, Theme theme) { 520 return AnimatorInflater.loadAnimator(res, theme, animResId, pathErrorScale); 521 } 522 } 523 524 private void setupAnimatorsForTarget(String name, Animator animator) { 525 Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name); 526 animator.setTarget(target); 527 if (mAnimatedVectorState.mTempAnimators == null) { 528 mAnimatedVectorState.mTempAnimators = new ArrayList<>(); 529 mAnimatedVectorState.mTargetNameMap = new ArrayMap<>(); 530 } 531 mAnimatedVectorState.mTempAnimators.add(animator); 532 mAnimatedVectorState.mTargetNameMap.put(animator, name); 533 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 534 Log.v(LOGTAG, "add animator for target " + name + " " + animator); 535 } 536 } 537 538 @Override 539 public boolean isRunning() { 540 return mAnimatedVectorState.mAnimatorSet.isRunning(); 541 } 542 543 private boolean isStarted() { 544 return mAnimatedVectorState.mAnimatorSet.isStarted(); 545 } 546 547 @Override 548 public void start() { 549 // If any one of the animator has not ended, do nothing. 550 if (isStarted()) { 551 return; 552 } 553 554 // Check for uninflated animators. We can remove this after we add 555 // support for Animator.applyTheme(). See comments in inflate(). 556 if (mAnimatedVectorState.mPendingAnims != null) { 557 // Attempt to load animators without applying a theme. 558 if (mRes != null) { 559 inflatePendingAnimators(mRes, null); 560 mRes = null; 561 } else { 562 Log.e(LOGTAG, "Failed to load animators. Either the AnimatedVectorDrawable must be" 563 + " created using a Resources object or applyTheme() must be called with" 564 + " a non-null Theme object."); 565 } 566 567 mAnimatedVectorState.mPendingAnims = null; 568 } 569 570 mAnimatedVectorState.mAnimatorSet.start(); 571 invalidateSelf(); 572 } 573 574 @Override 575 public void stop() { 576 mAnimatedVectorState.mAnimatorSet.end(); 577 } 578 579 /** 580 * Reverses ongoing animations or starts pending animations in reverse. 581 * <p> 582 * NOTE: Only works if all animations support reverse. Otherwise, this will 583 * do nothing. 584 * @hide 585 */ 586 public void reverse() { 587 // Only reverse when all the animators can be reverse. Otherwise, partially 588 // reverse is confusing. 589 if (!canReverse()) { 590 Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()"); 591 return; 592 } 593 mAnimatedVectorState.mAnimatorSet.reverse(); 594 } 595 596 /** 597 * @hide 598 */ 599 public boolean canReverse() { 600 return mAnimatedVectorState.mAnimatorSet.canReverse(); 601 } 602 603 private final Callback mCallback = new Callback() { 604 @Override 605 public void invalidateDrawable(Drawable who) { 606 invalidateSelf(); 607 } 608 609 @Override 610 public void scheduleDrawable(Drawable who, Runnable what, long when) { 611 scheduleSelf(what, when); 612 } 613 614 @Override 615 public void unscheduleDrawable(Drawable who, Runnable what) { 616 unscheduleSelf(what); 617 } 618 }; 619} 620