AnimatedVectorDrawable.java revision 1588f0ff54b88240b55eeaba97e67f0f1dee5f92
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; 34 35import com.android.internal.R; 36 37import org.xmlpull.v1.XmlPullParser; 38import org.xmlpull.v1.XmlPullParserException; 39 40import java.io.IOException; 41import java.util.ArrayList; 42import java.util.List; 43 44/** 45 * This class uses {@link android.animation.ObjectAnimator} and 46 * {@link android.animation.AnimatorSet} to animate the properties of a 47 * {@link android.graphics.drawable.VectorDrawable} to create an animated drawable. 48 * <p> 49 * AnimatedVectorDrawable are normally defined as 3 separate XML files. 50 * </p> 51 * <p> 52 * First is the XML file for {@link android.graphics.drawable.VectorDrawable}. 53 * Note that we allow the animation happen on the group's attributes and path's 54 * attributes, which requires they are uniquely named in this xml file. Groups 55 * and paths without animations do not need names. 56 * </p> 57 * <li>Here is a simple VectorDrawable in this vectordrawable.xml file. 58 * <pre> 59 * <vector xmlns:android="http://schemas.android.com/apk/res/android" 60 * android:height="64dp" 61 * android:width="64dp" 62 * android:viewportHeight="600" 63 * android:viewportWidth="600" > 64 * <group 65 * android:name="rotationGroup" 66 * android:pivotX="300.0" 67 * android:pivotY="300.0" 68 * android:rotation="45.0" > 69 * <path 70 * android:name="v" 71 * android:fillColor="#000000" 72 * android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" /> 73 * </group> 74 * </vector> 75 * </pre></li> 76 * <p> 77 * Second is the AnimatedVectorDrawable's xml file, which defines the target 78 * VectorDrawable, the target paths and groups to animate, the properties of the 79 * path and group to animate and the animations defined as the ObjectAnimators 80 * or AnimatorSets. 81 * </p> 82 * <li>Here is a simple AnimatedVectorDrawable defined in this avd.xml file. 83 * Note how we use the names to refer to the groups and paths in the vectordrawable.xml. 84 * <pre> 85 * <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" 86 * android:drawable="@drawable/vectordrawable" > 87 * <target 88 * android:name="rotationGroup" 89 * android:animation="@anim/rotation" /> 90 * <target 91 * android:name="v" 92 * android:animation="@anim/path_morph" /> 93 * </animated-vector> 94 * </pre></li> 95 * <p> 96 * Last is the Animator xml file, which is the same as a normal ObjectAnimator 97 * or AnimatorSet. 98 * To complete this example, here are the 2 animator files used in avd.xml: 99 * rotation.xml and path_morph.xml. 100 * </p> 101 * <li>Here is the rotation.xml, which will rotate the target group for 360 degrees. 102 * <pre> 103 * <objectAnimator 104 * android:duration="6000" 105 * android:propertyName="rotation" 106 * android:valueFrom="0" 107 * android:valueTo="360" /> 108 * </pre></li> 109 * <li>Here is the path_morph.xml, which will morph the path from one shape to 110 * the other. Note that the paths must be compatible for morphing. 111 * In more details, the paths should have exact same length of commands , and 112 * exact same length of parameters for each commands. 113 * Note that the path string are better stored in strings.xml for reusing. 114 * <pre> 115 * <set xmlns:android="http://schemas.android.com/apk/res/android"> 116 * <objectAnimator 117 * android:duration="3000" 118 * android:propertyName="pathData" 119 * android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z" 120 * android:valueTo="M300,70 l 0,-70 70,0 0,140 -70,0 z" 121 * android:valueType="pathType"/> 122 * </set> 123 * </pre></li> 124 * 125 * @attr ref android.R.styleable#AnimatedVectorDrawable_drawable 126 * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name 127 * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation 128 */ 129public class AnimatedVectorDrawable extends Drawable implements Animatable { 130 private static final String LOGTAG = AnimatedVectorDrawable.class.getSimpleName(); 131 132 private static final String ANIMATED_VECTOR = "animated-vector"; 133 private static final String TARGET = "target"; 134 135 private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false; 136 137 private AnimatedVectorDrawableState mAnimatedVectorState; 138 139 private boolean mMutated; 140 141 public AnimatedVectorDrawable() { 142 this(null, null); 143 } 144 145 private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) { 146 mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res); 147 } 148 149 @Override 150 public Drawable mutate() { 151 if (!mMutated && super.mutate() == this) { 152 mAnimatedVectorState = new AnimatedVectorDrawableState( 153 mAnimatedVectorState, mCallback, null); 154 mMutated = true; 155 } 156 return this; 157 } 158 159 /** 160 * @hide 161 */ 162 public void clearMutated() { 163 super.clearMutated(); 164 mAnimatedVectorState.mVectorDrawable.clearMutated(); 165 mMutated = false; 166 } 167 168 @Override 169 public ConstantState getConstantState() { 170 mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations(); 171 return mAnimatedVectorState; 172 } 173 174 @Override 175 public int getChangingConfigurations() { 176 return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations; 177 } 178 179 @Override 180 public void draw(Canvas canvas) { 181 mAnimatedVectorState.mVectorDrawable.draw(canvas); 182 if (isStarted()) { 183 invalidateSelf(); 184 } 185 } 186 187 @Override 188 protected void onBoundsChange(Rect bounds) { 189 mAnimatedVectorState.mVectorDrawable.setBounds(bounds); 190 } 191 192 @Override 193 protected boolean onStateChange(int[] state) { 194 return mAnimatedVectorState.mVectorDrawable.setState(state); 195 } 196 197 @Override 198 protected boolean onLevelChange(int level) { 199 return mAnimatedVectorState.mVectorDrawable.setLevel(level); 200 } 201 202 @Override 203 public boolean onLayoutDirectionChange(int layoutDirection) { 204 return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection); 205 } 206 207 @Override 208 public int getAlpha() { 209 return mAnimatedVectorState.mVectorDrawable.getAlpha(); 210 } 211 212 @Override 213 public void setAlpha(int alpha) { 214 mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); 215 } 216 217 @Override 218 public void setColorFilter(ColorFilter colorFilter) { 219 mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); 220 } 221 222 @Override 223 public void setTintList(ColorStateList tint) { 224 mAnimatedVectorState.mVectorDrawable.setTintList(tint); 225 } 226 227 @Override 228 public void setHotspot(float x, float y) { 229 mAnimatedVectorState.mVectorDrawable.setHotspot(x, y); 230 } 231 232 @Override 233 public void setHotspotBounds(int left, int top, int right, int bottom) { 234 mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom); 235 } 236 237 @Override 238 public void setTintMode(PorterDuff.Mode tintMode) { 239 mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); 240 } 241 242 @Override 243 public boolean setVisible(boolean visible, boolean restart) { 244 mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); 245 return super.setVisible(visible, restart); 246 } 247 248 @Override 249 public boolean isStateful() { 250 return mAnimatedVectorState.mVectorDrawable.isStateful(); 251 } 252 253 @Override 254 public int getOpacity() { 255 return mAnimatedVectorState.mVectorDrawable.getOpacity(); 256 } 257 258 @Override 259 public int getIntrinsicWidth() { 260 return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); 261 } 262 263 @Override 264 public int getIntrinsicHeight() { 265 return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); 266 } 267 268 @Override 269 public void getOutline(@NonNull Outline outline) { 270 mAnimatedVectorState.mVectorDrawable.getOutline(outline); 271 } 272 273 @Override 274 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 275 throws XmlPullParserException, IOException { 276 277 int eventType = parser.getEventType(); 278 float pathErrorScale = 1; 279 while (eventType != XmlPullParser.END_DOCUMENT) { 280 if (eventType == XmlPullParser.START_TAG) { 281 final String tagName = parser.getName(); 282 if (ANIMATED_VECTOR.equals(tagName)) { 283 final TypedArray a = obtainAttributes(res, theme, attrs, 284 R.styleable.AnimatedVectorDrawable); 285 int drawableRes = a.getResourceId( 286 R.styleable.AnimatedVectorDrawable_drawable, 0); 287 if (drawableRes != 0) { 288 VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable( 289 drawableRes, theme).mutate(); 290 vectorDrawable.setAllowCaching(false); 291 vectorDrawable.setCallback(mCallback); 292 pathErrorScale = vectorDrawable.getPixelSize(); 293 if (mAnimatedVectorState.mVectorDrawable != null) { 294 mAnimatedVectorState.mVectorDrawable.setCallback(null); 295 } 296 mAnimatedVectorState.mVectorDrawable = vectorDrawable; 297 } 298 a.recycle(); 299 } else if (TARGET.equals(tagName)) { 300 final TypedArray a = obtainAttributes(res, theme, attrs, 301 R.styleable.AnimatedVectorDrawableTarget); 302 final String target = a.getString( 303 R.styleable.AnimatedVectorDrawableTarget_name); 304 305 int id = a.getResourceId( 306 R.styleable.AnimatedVectorDrawableTarget_animation, 0); 307 if (id != 0) { 308 Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id, 309 pathErrorScale); 310 setupAnimatorsForTarget(target, objectAnimator); 311 } 312 a.recycle(); 313 } 314 } 315 316 eventType = parser.next(); 317 } 318 setupAnimatorSet(); 319 } 320 321 private void setupAnimatorSet() { 322 if (mAnimatedVectorState.mTempAnimators != null) { 323 mAnimatedVectorState.mAnimatorSet.playTogether(mAnimatedVectorState.mTempAnimators); 324 mAnimatedVectorState.mTempAnimators.clear(); 325 mAnimatedVectorState.mTempAnimators = null; 326 } 327 } 328 329 @Override 330 public boolean canApplyTheme() { 331 return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme()) 332 || super.canApplyTheme(); 333 } 334 335 @Override 336 public void applyTheme(Theme t) { 337 super.applyTheme(t); 338 339 final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable; 340 if (vectorDrawable != null && vectorDrawable.canApplyTheme()) { 341 vectorDrawable.applyTheme(t); 342 } 343 } 344 345 /** 346 * Adds a listener to the set of listeners that are sent events through the life of an 347 * animation. 348 * 349 * @param listener the listener to be added to the current set of listeners for this animation. 350 */ 351 public void addListener(AnimatorListener listener) { 352 mAnimatedVectorState.mAnimatorSet.addListener(listener); 353 } 354 355 /** 356 * Removes a listener from the set listening to this animation. 357 * 358 * @param listener the listener to be removed from the current set of listeners for this 359 * animation. 360 */ 361 public void removeListener(AnimatorListener listener) { 362 mAnimatedVectorState.mAnimatorSet.removeListener(listener); 363 } 364 365 /** 366 * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently 367 * listening for events on this <code>AnimatedVectorDrawable</code> object. 368 * 369 * @return List<AnimatorListener> The set of listeners. 370 */ 371 public List<AnimatorListener> getListeners() { 372 return mAnimatedVectorState.mAnimatorSet.getListeners(); 373 } 374 375 private static class AnimatedVectorDrawableState extends ConstantState { 376 int mChangingConfigurations; 377 VectorDrawable mVectorDrawable; 378 // Always have a valid animatorSet to handle all the listeners call. 379 AnimatorSet mAnimatorSet = new AnimatorSet(); 380 // When parsing the XML, we build individual animator and store in this array. At the end, 381 // we add this array into the mAnimatorSet. 382 private ArrayList<Animator> mTempAnimators; 383 ArrayMap<Animator, String> mTargetNameMap; 384 385 public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy, 386 Callback owner, Resources res) { 387 if (copy != null) { 388 mChangingConfigurations = copy.mChangingConfigurations; 389 if (copy.mVectorDrawable != null) { 390 final ConstantState cs = copy.mVectorDrawable.getConstantState(); 391 if (res != null) { 392 mVectorDrawable = (VectorDrawable) cs.newDrawable(res); 393 } else { 394 mVectorDrawable = (VectorDrawable) cs.newDrawable(); 395 } 396 mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate(); 397 mVectorDrawable.setCallback(owner); 398 mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection()); 399 mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); 400 mVectorDrawable.setAllowCaching(false); 401 } 402 if (copy.mAnimatorSet != null) { 403 final int numAnimators = copy.mTargetNameMap.size(); 404 // Deep copy a animator set, and then setup the target map again. 405 mAnimatorSet = copy.mAnimatorSet.clone(); 406 mTargetNameMap = new ArrayMap<Animator, String>(numAnimators); 407 // Since the new AnimatorSet is cloned from the old one, the order must be the 408 // same inside the array. 409 ArrayList<Animator> oldAnim = copy.mAnimatorSet.getChildAnimations(); 410 ArrayList<Animator> newAnim = mAnimatorSet.getChildAnimations(); 411 412 for (int i = 0; i < numAnimators; ++i) { 413 // Target name must be the same for new and old 414 String targetName = copy.mTargetNameMap.get(oldAnim.get(i)); 415 416 Object newTargetObject = mVectorDrawable.getTargetByName(targetName); 417 newAnim.get(i).setTarget(newTargetObject); 418 mTargetNameMap.put(newAnim.get(i), targetName); 419 } 420 } 421 } else { 422 mVectorDrawable = new VectorDrawable(); 423 } 424 } 425 426 @Override 427 public boolean canApplyTheme() { 428 return (mVectorDrawable != null && mVectorDrawable.canApplyTheme()) 429 || super.canApplyTheme(); 430 } 431 432 @Override 433 public Drawable newDrawable() { 434 return new AnimatedVectorDrawable(this, null); 435 } 436 437 @Override 438 public Drawable newDrawable(Resources res) { 439 return new AnimatedVectorDrawable(this, res); 440 } 441 442 @Override 443 public int getChangingConfigurations() { 444 return mChangingConfigurations; 445 } 446 } 447 448 private void setupAnimatorsForTarget(String name, Animator animator) { 449 Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name); 450 animator.setTarget(target); 451 if (mAnimatedVectorState.mTempAnimators == null) { 452 mAnimatedVectorState.mTempAnimators = new ArrayList<Animator>(); 453 mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>(); 454 } 455 mAnimatedVectorState.mTempAnimators.add(animator); 456 mAnimatedVectorState.mTargetNameMap.put(animator, name); 457 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 458 Log.v(LOGTAG, "add animator for target " + name + " " + animator); 459 } 460 } 461 462 @Override 463 public boolean isRunning() { 464 return mAnimatedVectorState.mAnimatorSet.isRunning(); 465 } 466 467 private boolean isStarted() { 468 return mAnimatedVectorState.mAnimatorSet.isStarted(); 469 } 470 471 @Override 472 public void start() { 473 // If any one of the animator has not ended, do nothing. 474 if (isStarted()) { 475 return; 476 } 477 mAnimatedVectorState.mAnimatorSet.start(); 478 invalidateSelf(); 479 } 480 481 @Override 482 public void stop() { 483 mAnimatedVectorState.mAnimatorSet.end(); 484 } 485 486 /** 487 * Reverses ongoing animations or starts pending animations in reverse. 488 * <p> 489 * NOTE: Only works if all animations support reverse. Otherwise, this will 490 * do nothing. 491 * @hide 492 */ 493 public void reverse() { 494 // Only reverse when all the animators can be reverse. Otherwise, partially 495 // reverse is confusing. 496 if (!canReverse()) { 497 Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()"); 498 return; 499 } 500 mAnimatedVectorState.mAnimatorSet.reverse(); 501 } 502 503 /** 504 * @hide 505 */ 506 public boolean canReverse() { 507 return mAnimatedVectorState.mAnimatorSet.canReverse(); 508 } 509 510 private final Callback mCallback = new Callback() { 511 @Override 512 public void invalidateDrawable(Drawable who) { 513 invalidateSelf(); 514 } 515 516 @Override 517 public void scheduleDrawable(Drawable who, Runnable what, long when) { 518 scheduleSelf(what, when); 519 } 520 521 @Override 522 public void unscheduleDrawable(Drawable who, Runnable what) { 523 unscheduleSelf(what); 524 } 525 }; 526} 527