AnimatedVectorDrawable.java revision dfa4646eca54511a28f2c61e1f4b9c697e05a913
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.content.res.ColorStateList; 23import android.content.res.Resources; 24import android.content.res.Resources.Theme; 25import android.content.res.TypedArray; 26import android.graphics.Canvas; 27import android.graphics.ColorFilter; 28import android.graphics.Outline; 29import android.graphics.PorterDuff; 30import android.graphics.Rect; 31import android.util.ArrayMap; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.view.View; 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.class.getSimpleName(); 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 private AnimatedVectorDrawableState mAnimatedVectorState; 139 140 private boolean mMutated; 141 142 public AnimatedVectorDrawable() { 143 this(null, null); 144 } 145 146 private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) { 147 mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res); 148 } 149 150 @Override 151 public Drawable mutate() { 152 if (!mMutated && super.mutate() == this) { 153 mAnimatedVectorState = new AnimatedVectorDrawableState( 154 mAnimatedVectorState, mCallback, null); 155 mMutated = true; 156 } 157 return this; 158 } 159 160 /** 161 * @hide 162 */ 163 public void clearMutated() { 164 super.clearMutated(); 165 mAnimatedVectorState.mVectorDrawable.clearMutated(); 166 mMutated = false; 167 } 168 169 @Override 170 public ConstantState getConstantState() { 171 mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations(); 172 return mAnimatedVectorState; 173 } 174 175 @Override 176 public int getChangingConfigurations() { 177 return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations(); 178 } 179 180 @Override 181 public void draw(Canvas canvas) { 182 mAnimatedVectorState.mVectorDrawable.draw(canvas); 183 if (isStarted()) { 184 invalidateSelf(); 185 } 186 } 187 188 @Override 189 protected void onBoundsChange(Rect bounds) { 190 mAnimatedVectorState.mVectorDrawable.setBounds(bounds); 191 } 192 193 @Override 194 protected boolean onStateChange(int[] state) { 195 return mAnimatedVectorState.mVectorDrawable.setState(state); 196 } 197 198 @Override 199 protected boolean onLevelChange(int level) { 200 return mAnimatedVectorState.mVectorDrawable.setLevel(level); 201 } 202 203 @Override 204 public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) { 205 return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection); 206 } 207 208 @Override 209 public int getAlpha() { 210 return mAnimatedVectorState.mVectorDrawable.getAlpha(); 211 } 212 213 @Override 214 public void setAlpha(int alpha) { 215 mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); 216 } 217 218 @Override 219 public void setColorFilter(ColorFilter colorFilter) { 220 mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); 221 } 222 223 @Override 224 public void setTintList(ColorStateList tint) { 225 mAnimatedVectorState.mVectorDrawable.setTintList(tint); 226 } 227 228 @Override 229 public void setHotspot(float x, float y) { 230 mAnimatedVectorState.mVectorDrawable.setHotspot(x, y); 231 } 232 233 @Override 234 public void setHotspotBounds(int left, int top, int right, int bottom) { 235 mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom); 236 } 237 238 @Override 239 public void setTintMode(PorterDuff.Mode tintMode) { 240 mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); 241 } 242 243 @Override 244 public boolean setVisible(boolean visible, boolean restart) { 245 mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); 246 return super.setVisible(visible, restart); 247 } 248 249 @Override 250 public boolean isStateful() { 251 return mAnimatedVectorState.mVectorDrawable.isStateful(); 252 } 253 254 @Override 255 public int getOpacity() { 256 return mAnimatedVectorState.mVectorDrawable.getOpacity(); 257 } 258 259 @Override 260 public int getIntrinsicWidth() { 261 return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); 262 } 263 264 @Override 265 public int getIntrinsicHeight() { 266 return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); 267 } 268 269 @Override 270 public void getOutline(@NonNull Outline outline) { 271 mAnimatedVectorState.mVectorDrawable.getOutline(outline); 272 } 273 274 @Override 275 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 276 throws XmlPullParserException, IOException { 277 278 int eventType = parser.getEventType(); 279 float pathErrorScale = 1; 280 while (eventType != XmlPullParser.END_DOCUMENT) { 281 if (eventType == XmlPullParser.START_TAG) { 282 final String tagName = parser.getName(); 283 if (ANIMATED_VECTOR.equals(tagName)) { 284 final TypedArray a = obtainAttributes(res, theme, attrs, 285 R.styleable.AnimatedVectorDrawable); 286 int drawableRes = a.getResourceId( 287 R.styleable.AnimatedVectorDrawable_drawable, 0); 288 if (drawableRes != 0) { 289 VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable( 290 drawableRes, theme).mutate(); 291 vectorDrawable.setAllowCaching(false); 292 vectorDrawable.setCallback(mCallback); 293 pathErrorScale = vectorDrawable.getPixelSize(); 294 if (mAnimatedVectorState.mVectorDrawable != null) { 295 mAnimatedVectorState.mVectorDrawable.setCallback(null); 296 } 297 mAnimatedVectorState.mVectorDrawable = vectorDrawable; 298 } 299 a.recycle(); 300 } else if (TARGET.equals(tagName)) { 301 final TypedArray a = obtainAttributes(res, theme, attrs, 302 R.styleable.AnimatedVectorDrawableTarget); 303 final String target = a.getString( 304 R.styleable.AnimatedVectorDrawableTarget_name); 305 306 int id = a.getResourceId( 307 R.styleable.AnimatedVectorDrawableTarget_animation, 0); 308 if (id != 0) { 309 Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id, 310 pathErrorScale); 311 setupAnimatorsForTarget(target, objectAnimator); 312 } 313 a.recycle(); 314 } 315 } 316 317 eventType = parser.next(); 318 } 319 setupAnimatorSet(); 320 } 321 322 private void setupAnimatorSet() { 323 if (mAnimatedVectorState.mTempAnimators != null) { 324 mAnimatedVectorState.mAnimatorSet.playTogether(mAnimatedVectorState.mTempAnimators); 325 mAnimatedVectorState.mTempAnimators.clear(); 326 mAnimatedVectorState.mTempAnimators = null; 327 } 328 } 329 330 @Override 331 public boolean canApplyTheme() { 332 return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme()) 333 || super.canApplyTheme(); 334 } 335 336 @Override 337 public void applyTheme(Theme t) { 338 super.applyTheme(t); 339 340 final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable; 341 if (vectorDrawable != null && vectorDrawable.canApplyTheme()) { 342 vectorDrawable.applyTheme(t); 343 } 344 } 345 346 /** 347 * Adds a listener to the set of listeners that are sent events through the life of an 348 * animation. 349 * 350 * @param listener the listener to be added to the current set of listeners for this animation. 351 */ 352 public void addListener(AnimatorListener listener) { 353 mAnimatedVectorState.mAnimatorSet.addListener(listener); 354 } 355 356 /** 357 * Removes a listener from the set listening to this animation. 358 * 359 * @param listener the listener to be removed from the current set of listeners for this 360 * animation. 361 */ 362 public void removeListener(AnimatorListener listener) { 363 mAnimatedVectorState.mAnimatorSet.removeListener(listener); 364 } 365 366 /** 367 * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently 368 * listening for events on this <code>AnimatedVectorDrawable</code> object. 369 * 370 * @return List<AnimatorListener> The set of listeners. 371 */ 372 public List<AnimatorListener> getListeners() { 373 return mAnimatedVectorState.mAnimatorSet.getListeners(); 374 } 375 376 private static class AnimatedVectorDrawableState extends ConstantState { 377 int mChangingConfigurations; 378 VectorDrawable mVectorDrawable; 379 // Always have a valid animatorSet to handle all the listeners call. 380 AnimatorSet mAnimatorSet = new AnimatorSet(); 381 // When parsing the XML, we build individual animator and store in this array. At the end, 382 // we add this array into the mAnimatorSet. 383 private ArrayList<Animator> mTempAnimators; 384 ArrayMap<Animator, String> mTargetNameMap; 385 386 public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy, 387 Callback owner, Resources res) { 388 if (copy != null) { 389 mChangingConfigurations = copy.mChangingConfigurations; 390 if (copy.mVectorDrawable != null) { 391 final ConstantState cs = copy.mVectorDrawable.getConstantState(); 392 if (res != null) { 393 mVectorDrawable = (VectorDrawable) cs.newDrawable(res); 394 } else { 395 mVectorDrawable = (VectorDrawable) cs.newDrawable(); 396 } 397 mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate(); 398 mVectorDrawable.setCallback(owner); 399 mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection()); 400 mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); 401 mVectorDrawable.setAllowCaching(false); 402 } 403 if (copy.mAnimatorSet != null) { 404 final int numAnimators = copy.mTargetNameMap.size(); 405 // Deep copy a animator set, and then setup the target map again. 406 mAnimatorSet = copy.mAnimatorSet.clone(); 407 mTargetNameMap = new ArrayMap<Animator, String>(numAnimators); 408 // Since the new AnimatorSet is cloned from the old one, the order must be the 409 // same inside the array. 410 ArrayList<Animator> oldAnim = copy.mAnimatorSet.getChildAnimations(); 411 ArrayList<Animator> newAnim = mAnimatorSet.getChildAnimations(); 412 413 for (int i = 0; i < numAnimators; ++i) { 414 // Target name must be the same for new and old 415 String targetName = copy.mTargetNameMap.get(oldAnim.get(i)); 416 417 Object newTargetObject = mVectorDrawable.getTargetByName(targetName); 418 newAnim.get(i).setTarget(newTargetObject); 419 mTargetNameMap.put(newAnim.get(i), targetName); 420 } 421 } 422 } else { 423 mVectorDrawable = new VectorDrawable(); 424 } 425 } 426 427 @Override 428 public boolean canApplyTheme() { 429 return (mVectorDrawable != null && mVectorDrawable.canApplyTheme()) 430 || super.canApplyTheme(); 431 } 432 433 @Override 434 public Drawable newDrawable() { 435 return new AnimatedVectorDrawable(this, null); 436 } 437 438 @Override 439 public Drawable newDrawable(Resources res) { 440 return new AnimatedVectorDrawable(this, res); 441 } 442 443 @Override 444 public int getChangingConfigurations() { 445 return mChangingConfigurations; 446 } 447 } 448 449 private void setupAnimatorsForTarget(String name, Animator animator) { 450 Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name); 451 animator.setTarget(target); 452 if (mAnimatedVectorState.mTempAnimators == null) { 453 mAnimatedVectorState.mTempAnimators = new ArrayList<Animator>(); 454 mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>(); 455 } 456 mAnimatedVectorState.mTempAnimators.add(animator); 457 mAnimatedVectorState.mTargetNameMap.put(animator, name); 458 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 459 Log.v(LOGTAG, "add animator for target " + name + " " + animator); 460 } 461 } 462 463 @Override 464 public boolean isRunning() { 465 return mAnimatedVectorState.mAnimatorSet.isRunning(); 466 } 467 468 private boolean isStarted() { 469 return mAnimatedVectorState.mAnimatorSet.isStarted(); 470 } 471 472 @Override 473 public void start() { 474 // If any one of the animator has not ended, do nothing. 475 if (isStarted()) { 476 return; 477 } 478 mAnimatedVectorState.mAnimatorSet.start(); 479 invalidateSelf(); 480 } 481 482 @Override 483 public void stop() { 484 mAnimatedVectorState.mAnimatorSet.end(); 485 } 486 487 /** 488 * Reverses ongoing animations or starts pending animations in reverse. 489 * <p> 490 * NOTE: Only works if all animations support reverse. Otherwise, this will 491 * do nothing. 492 * @hide 493 */ 494 public void reverse() { 495 // Only reverse when all the animators can be reverse. Otherwise, partially 496 // reverse is confusing. 497 if (!canReverse()) { 498 Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()"); 499 return; 500 } 501 mAnimatedVectorState.mAnimatorSet.reverse(); 502 } 503 504 /** 505 * @hide 506 */ 507 public boolean canReverse() { 508 return mAnimatedVectorState.mAnimatorSet.canReverse(); 509 } 510 511 private final Callback mCallback = new Callback() { 512 @Override 513 public void invalidateDrawable(Drawable who) { 514 invalidateSelf(); 515 } 516 517 @Override 518 public void scheduleDrawable(Drawable who, Runnable what, long when) { 519 scheduleSelf(what, when); 520 } 521 522 @Override 523 public void unscheduleDrawable(Drawable who, Runnable what) { 524 unscheduleSelf(what); 525 } 526 }; 527} 528