1/* 2 * Copyright (C) 2015 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.support.graphics.drawable; 16 17import android.animation.Animator; 18import android.animation.AnimatorInflater; 19import android.animation.AnimatorSet; 20import android.animation.ArgbEvaluator; 21import android.animation.ObjectAnimator; 22import android.annotation.TargetApi; 23import android.content.Context; 24import android.content.res.ColorStateList; 25import android.content.res.Resources; 26import android.content.res.Resources.Theme; 27import android.content.res.TypedArray; 28import android.graphics.Canvas; 29import android.graphics.ColorFilter; 30import android.graphics.PorterDuff; 31import android.graphics.Rect; 32import android.graphics.drawable.Animatable; 33import android.graphics.drawable.AnimatedVectorDrawable; 34import android.graphics.drawable.Drawable; 35import android.os.Build; 36import android.support.annotation.DrawableRes; 37import android.support.annotation.NonNull; 38import android.support.annotation.Nullable; 39import android.support.v4.content.res.ResourcesCompat; 40import android.support.v4.graphics.drawable.DrawableCompat; 41import android.support.v4.util.ArrayMap; 42import android.util.AttributeSet; 43import android.util.Log; 44import android.util.Xml; 45 46import org.xmlpull.v1.XmlPullParser; 47import org.xmlpull.v1.XmlPullParserException; 48 49import java.io.IOException; 50import java.util.ArrayList; 51import java.util.List; 52 53/** 54 * For API 24 and above, this class is delegating to the framework's {@link AnimatedVectorDrawable}. 55 * For older API version, this class uses {@link android.animation.ObjectAnimator} and 56 * {@link android.animation.AnimatorSet} to animate the properties of a 57 * {@link VectorDrawableCompat} to create an animated drawable. 58 * <p> 59 * AnimatedVectorDrawableCompat are defined in the same XML format as {@link AnimatedVectorDrawable}. 60 * </p> 61 * You can always create a AnimatedVectorDrawableCompat object and use it as a Drawable by the Java 62 * API. In order to refer to AnimatedVectorDrawableCompat inside a XML file, you can use 63 * app:srcCompat attribute in AppCompat library's ImageButton or ImageView. 64 */ 65@TargetApi(Build.VERSION_CODES.LOLLIPOP) 66public class AnimatedVectorDrawableCompat extends VectorDrawableCommon implements Animatable { 67 private static final String LOGTAG = "AnimatedVDCompat"; 68 69 private static final String ANIMATED_VECTOR = "animated-vector"; 70 private static final String TARGET = "target"; 71 72 private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false; 73 74 private AnimatedVectorDrawableCompatState mAnimatedVectorState; 75 76 private Context mContext; 77 78 private ArgbEvaluator mArgbEvaluator = null; 79 80 AnimatedVectorDrawableDelegateState mCachedConstantStateDelegate; 81 82 AnimatedVectorDrawableCompat() { 83 this(null, null, null); 84 } 85 86 private AnimatedVectorDrawableCompat(@Nullable Context context) { 87 this(context, null, null); 88 } 89 90 private AnimatedVectorDrawableCompat(@Nullable Context context, 91 @Nullable AnimatedVectorDrawableCompatState state, 92 @Nullable Resources res) { 93 mContext = context; 94 if (state != null) { 95 mAnimatedVectorState = state; 96 } else { 97 mAnimatedVectorState = new AnimatedVectorDrawableCompatState(context, state, mCallback, 98 res); 99 } 100 } 101 102 @Override 103 public Drawable mutate() { 104 if (mDelegateDrawable != null) { 105 mDelegateDrawable.mutate(); 106 return this; 107 } 108 throw new IllegalStateException("Mutate() is not supported for older platform"); 109 } 110 111 112 /** 113 * Create a AnimatedVectorDrawableCompat object. 114 * 115 * @param context the context for creating the animators. 116 * @param resId the resource ID for AnimatedVectorDrawableCompat object. 117 * @return a new AnimatedVectorDrawableCompat or null if parsing error is found. 118 */ 119 @Nullable 120 public static AnimatedVectorDrawableCompat create(@NonNull Context context, 121 @DrawableRes int resId) { 122 if (Build.VERSION.SDK_INT >= 24) { 123 final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context); 124 drawable.mDelegateDrawable = ResourcesCompat.getDrawable(context.getResources(), resId, 125 context.getTheme()); 126 drawable.mDelegateDrawable.setCallback(drawable.mCallback); 127 drawable.mCachedConstantStateDelegate = new AnimatedVectorDrawableDelegateState( 128 drawable.mDelegateDrawable.getConstantState()); 129 return drawable; 130 } 131 Resources resources = context.getResources(); 132 try { 133 final XmlPullParser parser = resources.getXml(resId); 134 final AttributeSet attrs = Xml.asAttributeSet(parser); 135 int type; 136 while ((type = parser.next()) != XmlPullParser.START_TAG 137 && type != XmlPullParser.END_DOCUMENT) { 138 // Empty loop 139 } 140 if (type != XmlPullParser.START_TAG) { 141 throw new XmlPullParserException("No start tag found"); 142 } 143 return createFromXmlInner(context, context.getResources(), parser, attrs, 144 context.getTheme()); 145 } catch (XmlPullParserException e) { 146 Log.e(LOGTAG, "parser error", e); 147 } catch (IOException e) { 148 Log.e(LOGTAG, "parser error", e); 149 } 150 return null; 151 } 152 153 /** 154 * Create a AnimatedVectorDrawableCompat from inside an XML document using an optional 155 * {@link Theme}. Called on a parser positioned at a tag in an XML 156 * document, tries to create a Drawable from that tag. Returns {@code null} 157 * if the tag is not a valid drawable. 158 */ 159 public static AnimatedVectorDrawableCompat createFromXmlInner(Context context, Resources r, 160 XmlPullParser parser, AttributeSet attrs, Theme theme) 161 throws XmlPullParserException, IOException { 162 final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context); 163 drawable.inflate(r, parser, attrs, theme); 164 return drawable; 165 } 166 167 /** 168 * {@inheritDoc} 169 * <strong>Note</strong> that we don't support constant state when SDK < 24. 170 * Make sure you check the return value before using it. 171 */ 172 @Override 173 public ConstantState getConstantState() { 174 if (mDelegateDrawable != null) { 175 return new AnimatedVectorDrawableDelegateState(mDelegateDrawable.getConstantState()); 176 } 177 // We can't support constant state in older platform. 178 // We need Context to create the animator, and we can't save the context in the constant 179 // state. 180 return null; 181 } 182 183 @Override 184 public int getChangingConfigurations() { 185 if (mDelegateDrawable != null) { 186 return mDelegateDrawable.getChangingConfigurations(); 187 } 188 return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations; 189 } 190 191 @Override 192 public void draw(Canvas canvas) { 193 if (mDelegateDrawable != null) { 194 mDelegateDrawable.draw(canvas); 195 return; 196 } 197 mAnimatedVectorState.mVectorDrawable.draw(canvas); 198 if (isStarted()) { 199 invalidateSelf(); 200 } 201 } 202 203 @Override 204 protected void onBoundsChange(Rect bounds) { 205 if (mDelegateDrawable != null) { 206 mDelegateDrawable.setBounds(bounds); 207 return; 208 } 209 mAnimatedVectorState.mVectorDrawable.setBounds(bounds); 210 } 211 212 @Override 213 protected boolean onStateChange(int[] state) { 214 if (mDelegateDrawable != null) { 215 return mDelegateDrawable.setState(state); 216 } 217 return mAnimatedVectorState.mVectorDrawable.setState(state); 218 } 219 220 @Override 221 protected boolean onLevelChange(int level) { 222 if (mDelegateDrawable != null) { 223 return mDelegateDrawable.setLevel(level); 224 } 225 return mAnimatedVectorState.mVectorDrawable.setLevel(level); 226 } 227 228 @Override 229 public int getAlpha() { 230 if (mDelegateDrawable != null) { 231 return DrawableCompat.getAlpha(mDelegateDrawable); 232 } 233 return mAnimatedVectorState.mVectorDrawable.getAlpha(); 234 } 235 236 @Override 237 public void setAlpha(int alpha) { 238 if (mDelegateDrawable != null) { 239 mDelegateDrawable.setAlpha(alpha); 240 return; 241 } 242 mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); 243 } 244 245 @Override 246 public void setColorFilter(ColorFilter colorFilter) { 247 if (mDelegateDrawable != null) { 248 mDelegateDrawable.setColorFilter(colorFilter); 249 return; 250 } 251 mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); 252 } 253 254 @Override 255 public void setTint(int tint) { 256 if (mDelegateDrawable != null) { 257 DrawableCompat.setTint(mDelegateDrawable, tint); 258 return; 259 } 260 261 mAnimatedVectorState.mVectorDrawable.setTint(tint); 262 } 263 264 @Override 265 public void setTintList(ColorStateList tint) { 266 if (mDelegateDrawable != null) { 267 DrawableCompat.setTintList(mDelegateDrawable, tint); 268 return; 269 } 270 271 mAnimatedVectorState.mVectorDrawable.setTintList(tint); 272 } 273 274 @Override 275 public void setTintMode(PorterDuff.Mode tintMode) { 276 if (mDelegateDrawable != null) { 277 DrawableCompat.setTintMode(mDelegateDrawable, tintMode); 278 return; 279 } 280 281 mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); 282 } 283 284 @Override 285 public boolean setVisible(boolean visible, boolean restart) { 286 if (mDelegateDrawable != null) { 287 return mDelegateDrawable.setVisible(visible, restart); 288 } 289 mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); 290 return super.setVisible(visible, restart); 291 } 292 293 @Override 294 public boolean isStateful() { 295 if (mDelegateDrawable != null) { 296 return mDelegateDrawable.isStateful(); 297 } 298 return mAnimatedVectorState.mVectorDrawable.isStateful(); 299 } 300 301 @Override 302 public int getOpacity() { 303 if (mDelegateDrawable != null) { 304 return mDelegateDrawable.getOpacity(); 305 } 306 return mAnimatedVectorState.mVectorDrawable.getOpacity(); 307 } 308 309 @Override 310 public int getIntrinsicWidth() { 311 if (mDelegateDrawable != null) { 312 return mDelegateDrawable.getIntrinsicWidth(); 313 } 314 return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); 315 } 316 317 @Override 318 public int getIntrinsicHeight() { 319 if (mDelegateDrawable != null) { 320 return mDelegateDrawable.getIntrinsicHeight(); 321 } 322 return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); 323 } 324 325 @Override 326 public boolean isAutoMirrored() { 327 if (mDelegateDrawable != null) { 328 return DrawableCompat.isAutoMirrored(mDelegateDrawable); 329 } 330 return mAnimatedVectorState.mVectorDrawable.isAutoMirrored(); 331 } 332 333 @Override 334 public void setAutoMirrored(boolean mirrored) { 335 if (mDelegateDrawable != null) { 336 mDelegateDrawable.setAutoMirrored(mirrored); 337 return; 338 } 339 mAnimatedVectorState.mVectorDrawable.setAutoMirrored(mirrored); 340 } 341 342 /** 343 * Obtains styled attributes from the theme, if available, or unstyled 344 * resources if the theme is null. 345 */ 346 static TypedArray obtainAttributes( 347 Resources res, Theme theme, AttributeSet set, int[] attrs) { 348 if (theme == null) { 349 return res.obtainAttributes(set, attrs); 350 } 351 return theme.obtainStyledAttributes(set, attrs, 0, 0); 352 } 353 354 @Override 355 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 356 throws XmlPullParserException, IOException { 357 if (mDelegateDrawable != null) { 358 DrawableCompat.inflate(mDelegateDrawable, res, parser, attrs, theme); 359 return; 360 } 361 int eventType = parser.getEventType(); 362 final int innerDepth = parser.getDepth() + 1; 363 364 // Parse everything until the end of the animated-vector element. 365 while (eventType != XmlPullParser.END_DOCUMENT 366 && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) { 367 if (eventType == XmlPullParser.START_TAG) { 368 final String tagName = parser.getName(); 369 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 370 Log.v(LOGTAG, "tagName is " + tagName); 371 } 372 if (ANIMATED_VECTOR.equals(tagName)) { 373 final TypedArray a = 374 obtainAttributes(res, theme, attrs, 375 AndroidResources.styleable_AnimatedVectorDrawable); 376 377 int drawableRes = a.getResourceId( 378 AndroidResources.styleable_AnimatedVectorDrawable_drawable, 0); 379 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 380 Log.v(LOGTAG, "drawableRes is " + drawableRes); 381 } 382 if (drawableRes != 0) { 383 VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(res, 384 drawableRes, theme); 385 vectorDrawable.setAllowCaching(false); 386 vectorDrawable.setCallback(mCallback); 387 if (mAnimatedVectorState.mVectorDrawable != null) { 388 mAnimatedVectorState.mVectorDrawable.setCallback(null); 389 } 390 mAnimatedVectorState.mVectorDrawable = vectorDrawable; 391 } 392 a.recycle(); 393 } else if (TARGET.equals(tagName)) { 394 final TypedArray a = 395 res.obtainAttributes(attrs, 396 AndroidResources.styleable_AnimatedVectorDrawableTarget); 397 final String target = a.getString( 398 AndroidResources.styleable_AnimatedVectorDrawableTarget_name); 399 400 int id = a.getResourceId( 401 AndroidResources.styleable_AnimatedVectorDrawableTarget_animation, 0); 402 if (id != 0) { 403 if (mContext != null) { 404 Animator objectAnimator = AnimatorInflater.loadAnimator(mContext, id); 405 setupAnimatorsForTarget(target, objectAnimator); 406 } else { 407 throw new IllegalStateException("Context can't be null when inflating" + 408 " animators"); 409 } 410 } 411 a.recycle(); 412 } 413 } 414 415 eventType = parser.next(); 416 } 417 } 418 419 @Override 420 public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs) 421 throws XmlPullParserException, IOException { 422 inflate(res, parser, attrs, null); 423 } 424 425 @Override 426 public void applyTheme(Theme t) { 427 if (mDelegateDrawable != null) { 428 DrawableCompat.applyTheme(mDelegateDrawable, t); 429 return; 430 } 431 // TODO: support theming in older platform. 432 return; 433 } 434 435 @Override 436 public boolean canApplyTheme() { 437 if (mDelegateDrawable != null) { 438 return DrawableCompat.canApplyTheme(mDelegateDrawable); 439 } 440 // TODO: support theming in older platform. 441 return false; 442 } 443 444 /** 445 * Constant state for delegating the creating drawable job. 446 * Instead of creating a VectorDrawable, create a VectorDrawableCompat instance which contains 447 * a delegated VectorDrawable instance. 448 */ 449 private static class AnimatedVectorDrawableDelegateState extends ConstantState { 450 private final ConstantState mDelegateState; 451 452 public AnimatedVectorDrawableDelegateState(ConstantState state) { 453 mDelegateState = state; 454 } 455 456 @Override 457 public Drawable newDrawable() { 458 AnimatedVectorDrawableCompat drawableCompat = 459 new AnimatedVectorDrawableCompat(); 460 drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(); 461 drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback); 462 return drawableCompat; 463 } 464 465 @Override 466 public Drawable newDrawable(Resources res) { 467 AnimatedVectorDrawableCompat drawableCompat = 468 new AnimatedVectorDrawableCompat(); 469 drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res); 470 drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback); 471 return drawableCompat; 472 } 473 474 @Override 475 public Drawable newDrawable(Resources res, Theme theme) { 476 AnimatedVectorDrawableCompat drawableCompat = 477 new AnimatedVectorDrawableCompat(); 478 drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res, theme); 479 drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback); 480 return drawableCompat; 481 } 482 483 @Override 484 public boolean canApplyTheme() { 485 return mDelegateState.canApplyTheme(); 486 } 487 488 @Override 489 public int getChangingConfigurations() { 490 return mDelegateState.getChangingConfigurations(); 491 } 492 } 493 494 private static class AnimatedVectorDrawableCompatState extends ConstantState { 495 int mChangingConfigurations; 496 VectorDrawableCompat mVectorDrawable; 497 ArrayList<Animator> mAnimators; 498 ArrayMap<Animator, String> mTargetNameMap; 499 500 public AnimatedVectorDrawableCompatState(Context context, 501 AnimatedVectorDrawableCompatState copy, Callback owner, Resources res) { 502 if (copy != null) { 503 mChangingConfigurations = copy.mChangingConfigurations; 504 if (copy.mVectorDrawable != null) { 505 final ConstantState cs = copy.mVectorDrawable.getConstantState(); 506 if (res != null) { 507 mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(res); 508 } else { 509 mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(); 510 } 511 mVectorDrawable = (VectorDrawableCompat) mVectorDrawable.mutate(); 512 mVectorDrawable.setCallback(owner); 513 mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); 514 mVectorDrawable.setAllowCaching(false); 515 } 516 if (copy.mAnimators != null) { 517 final int numAnimators = copy.mAnimators.size(); 518 mAnimators = new ArrayList<Animator>(numAnimators); 519 mTargetNameMap = new ArrayMap<Animator, String>(numAnimators); 520 for (int i = 0; i < numAnimators; ++i) { 521 Animator anim = copy.mAnimators.get(i); 522 Animator animClone = anim.clone(); 523 String targetName = copy.mTargetNameMap.get(anim); 524 Object targetObject = mVectorDrawable.getTargetByName(targetName); 525 animClone.setTarget(targetObject); 526 mAnimators.add(animClone); 527 mTargetNameMap.put(animClone, targetName); 528 } 529 } 530 } 531 } 532 533 @Override 534 public Drawable newDrawable() { 535 throw new IllegalStateException("No constant state support for SDK < 24."); 536 } 537 538 @Override 539 public Drawable newDrawable(Resources res) { 540 throw new IllegalStateException("No constant state support for SDK < 24."); 541 } 542 543 @Override 544 public int getChangingConfigurations() { 545 return mChangingConfigurations; 546 } 547 } 548 549 /** 550 * Utility function to fix color interpolation prior to Lollipop. Without this fix, colors 551 * are evaluated as raw integers instead of as colors, which leads to artifacts during 552 * fillColor animations. 553 */ 554 private void setupColorAnimator(Animator animator) { 555 if (animator instanceof AnimatorSet) { 556 List<Animator> childAnimators = ((AnimatorSet) animator).getChildAnimations(); 557 if (childAnimators != null) { 558 for (int i = 0; i < childAnimators.size(); ++i) { 559 setupColorAnimator(childAnimators.get(i)); 560 } 561 } 562 } 563 if (animator instanceof ObjectAnimator) { 564 ObjectAnimator objectAnim = (ObjectAnimator) animator; 565 final String propertyName = objectAnim.getPropertyName(); 566 if ("fillColor".equals(propertyName) || "strokeColor".equals(propertyName)) { 567 if (mArgbEvaluator == null) { 568 mArgbEvaluator = new ArgbEvaluator(); 569 } 570 objectAnim.setEvaluator(mArgbEvaluator); 571 } 572 } 573 } 574 575 private void setupAnimatorsForTarget(String name, Animator animator) { 576 Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name); 577 animator.setTarget(target); 578 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 579 setupColorAnimator(animator); 580 } 581 if (mAnimatedVectorState.mAnimators == null) { 582 mAnimatedVectorState.mAnimators = new ArrayList<Animator>(); 583 mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>(); 584 } 585 mAnimatedVectorState.mAnimators.add(animator); 586 mAnimatedVectorState.mTargetNameMap.put(animator, name); 587 if (DBG_ANIMATION_VECTOR_DRAWABLE) { 588 Log.v(LOGTAG, "add animator for target " + name + " " + animator); 589 } 590 } 591 592 @Override 593 public boolean isRunning() { 594 if (mDelegateDrawable != null) { 595 return ((AnimatedVectorDrawable) mDelegateDrawable).isRunning(); 596 } 597 final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; 598 final int size = animators.size(); 599 for (int i = 0; i < size; i++) { 600 final Animator animator = animators.get(i); 601 if (animator.isRunning()) { 602 return true; 603 } 604 } 605 return false; 606 } 607 608 private boolean isStarted() { 609 final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; 610 if (animators == null) { 611 return false; 612 } 613 final int size = animators.size(); 614 for (int i = 0; i < size; i++) { 615 final Animator animator = animators.get(i); 616 if (animator.isRunning()) { 617 return true; 618 } 619 } 620 return false; 621 } 622 623 @Override 624 public void start() { 625 if (mDelegateDrawable != null) { 626 ((AnimatedVectorDrawable) mDelegateDrawable).start(); 627 return; 628 } 629 // If any one of the animator has not ended, do nothing. 630 if (isStarted()) { 631 return; 632 } 633 // Otherwise, kick off every animator. 634 final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; 635 final int size = animators.size(); 636 for (int i = 0; i < size; i++) { 637 final Animator animator = animators.get(i); 638 animator.start(); 639 } 640 invalidateSelf(); 641 } 642 643 @Override 644 public void stop() { 645 if (mDelegateDrawable != null) { 646 ((AnimatedVectorDrawable) mDelegateDrawable).stop(); 647 return; 648 } 649 final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators; 650 final int size = animators.size(); 651 for (int i = 0; i < size; i++) { 652 final Animator animator = animators.get(i); 653 animator.end(); 654 } 655 } 656 657 final Callback mCallback = new Callback() { 658 @Override 659 public void invalidateDrawable(Drawable who) { 660 invalidateSelf(); 661 } 662 663 @Override 664 public void scheduleDrawable(Drawable who, Runnable what, long when) { 665 scheduleSelf(what, when); 666 } 667 668 @Override 669 public void unscheduleDrawable(Drawable who, Runnable what) { 670 unscheduleSelf(what); 671 } 672 }; 673} 674