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