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