AnimatedStateListDrawable.java revision d6570d11e4d1e43c2cfe1d10e27a7786c4283169
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.graphics.drawable; 18 19import android.animation.ObjectAnimator; 20import android.animation.TimeInterpolator; 21import android.annotation.NonNull; 22import android.annotation.Nullable; 23import android.content.res.Resources; 24import android.content.res.Resources.Theme; 25import android.content.res.TypedArray; 26import android.util.AttributeSet; 27import android.util.Log; 28import android.util.LongSparseLongArray; 29import android.util.SparseIntArray; 30import android.util.StateSet; 31 32import com.android.internal.R; 33 34import org.xmlpull.v1.XmlPullParser; 35import org.xmlpull.v1.XmlPullParserException; 36 37import java.io.IOException; 38 39/** 40 * Drawable containing a set of Drawable keyframes where the currently displayed 41 * keyframe is chosen based on the current state set. Animations between 42 * keyframes may optionally be defined using transition elements. 43 * <p> 44 * This drawable can be defined in an XML file with the <code> 45 * <animated-selector></code> element. Each keyframe Drawable is defined in a 46 * nested <code><item></code> element. Transitions are defined in a nested 47 * <code><transition></code> element. 48 * 49 * @attr ref android.R.styleable#DrawableStates_state_focused 50 * @attr ref android.R.styleable#DrawableStates_state_window_focused 51 * @attr ref android.R.styleable#DrawableStates_state_enabled 52 * @attr ref android.R.styleable#DrawableStates_state_checkable 53 * @attr ref android.R.styleable#DrawableStates_state_checked 54 * @attr ref android.R.styleable#DrawableStates_state_selected 55 * @attr ref android.R.styleable#DrawableStates_state_activated 56 * @attr ref android.R.styleable#DrawableStates_state_active 57 * @attr ref android.R.styleable#DrawableStates_state_single 58 * @attr ref android.R.styleable#DrawableStates_state_first 59 * @attr ref android.R.styleable#DrawableStates_state_middle 60 * @attr ref android.R.styleable#DrawableStates_state_last 61 * @attr ref android.R.styleable#DrawableStates_state_pressed 62 */ 63public class AnimatedStateListDrawable extends StateListDrawable { 64 private static final String LOGTAG = AnimatedStateListDrawable.class.getSimpleName(); 65 66 private static final String ELEMENT_TRANSITION = "transition"; 67 private static final String ELEMENT_ITEM = "item"; 68 69 private AnimatedStateListState mState; 70 71 /** The currently running transition, if any. */ 72 private Transition mTransition; 73 74 /** Index to be set after the transition ends. */ 75 private int mTransitionToIndex = -1; 76 77 /** Index away from which we are transitioning. */ 78 private int mTransitionFromIndex = -1; 79 80 private boolean mMutated; 81 82 public AnimatedStateListDrawable() { 83 this(null, null); 84 } 85 86 @Override 87 public boolean setVisible(boolean visible, boolean restart) { 88 final boolean changed = super.setVisible(visible, restart); 89 90 if (mTransition != null && (changed || restart)) { 91 if (visible) { 92 mTransition.start(); 93 } else { 94 // Ensure we're showing the correct state when visible. 95 jumpToCurrentState(); 96 } 97 } 98 99 return changed; 100 } 101 102 /** 103 * Add a new drawable to the set of keyframes. 104 * 105 * @param stateSet An array of resource IDs to associate with the keyframe 106 * @param drawable The drawable to show when in the specified state, may not be null 107 * @param id The unique identifier for the keyframe 108 */ 109 public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { 110 if (drawable == null) { 111 throw new IllegalArgumentException("Drawable must not be null"); 112 } 113 114 mState.addStateSet(stateSet, drawable, id); 115 onStateChange(getState()); 116 } 117 118 /** 119 * Adds a new transition between keyframes. 120 * 121 * @param fromId Unique identifier of the starting keyframe 122 * @param toId Unique identifier of the ending keyframe 123 * @param transition An {@link Animatable} drawable to use as a transition, may not be null 124 * @param reversible Whether the transition can be reversed 125 */ 126 public <T extends Drawable & Animatable> void addTransition(int fromId, int toId, 127 @NonNull T transition, boolean reversible) { 128 if (transition == null) { 129 throw new IllegalArgumentException("Transition drawable must not be null"); 130 } 131 132 mState.addTransition(fromId, toId, transition, reversible); 133 } 134 135 @Override 136 public boolean isStateful() { 137 return true; 138 } 139 140 @Override 141 protected boolean onStateChange(int[] stateSet) { 142 // If we're not already at the target index, either attempt to find a 143 // valid transition to it or jump directly there. 144 final int targetIndex = mState.indexOfKeyframe(stateSet); 145 final boolean changedIndex = targetIndex != getCurrentIndex() 146 && (selectTransition(targetIndex) || selectDrawable(targetIndex)); 147 148 // Always call super.onStateChanged() to propagate the state change to 149 // the current drawable. 150 return super.onStateChange(stateSet) || changedIndex; 151 } 152 153 private boolean selectTransition(int toIndex) { 154 final int fromIndex; 155 final Transition currentTransition = mTransition; 156 if (currentTransition != null) { 157 if (toIndex == mTransitionToIndex) { 158 // Already animating to that keyframe. 159 return true; 160 } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) { 161 // Reverse the current animation. 162 currentTransition.reverse(); 163 mTransitionToIndex = mTransitionFromIndex; 164 mTransitionFromIndex = toIndex; 165 return true; 166 } 167 168 // Start the next transition from the end of the current one. 169 fromIndex = mTransitionToIndex; 170 171 // Changing animation, end the current animation. 172 currentTransition.stop(); 173 } else { 174 fromIndex = getCurrentIndex(); 175 } 176 177 // Reset state. 178 mTransition = null; 179 mTransitionFromIndex = -1; 180 mTransitionToIndex = -1; 181 182 final AnimatedStateListState state = mState; 183 final int fromId = state.getKeyframeIdAt(fromIndex); 184 final int toId = state.getKeyframeIdAt(toIndex); 185 if (toId == 0 || fromId == 0) { 186 // Missing a keyframe ID. 187 return false; 188 } 189 190 final int transitionIndex = state.indexOfTransition(fromId, toId); 191 if (transitionIndex < 0) { 192 // Couldn't select a transition. 193 return false; 194 } 195 196 // This may fail if we're already on the transition, but that's okay! 197 selectDrawable(transitionIndex); 198 199 final Transition transition; 200 final Drawable d = getCurrent(); 201 if (d instanceof AnimationDrawable) { 202 final boolean reversed = state.isTransitionReversed(fromId, toId); 203 transition = new AnimationDrawableTransition((AnimationDrawable) d, reversed); 204 } else if (d instanceof AnimatedVectorDrawable) { 205 final boolean reversed = state.isTransitionReversed(fromId, toId); 206 transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d, reversed); 207 } else if (d instanceof Animatable) { 208 transition = new AnimatableTransition((Animatable) d); 209 } else { 210 // We don't know how to animate this transition. 211 return false; 212 } 213 214 transition.start(); 215 216 mTransition = transition; 217 mTransitionFromIndex = fromIndex; 218 mTransitionToIndex = toIndex; 219 return true; 220 } 221 222 private static abstract class Transition { 223 public abstract void start(); 224 public abstract void stop(); 225 226 public void reverse() { 227 // Not supported by default. 228 } 229 230 public boolean canReverse() { 231 return false; 232 } 233 } 234 235 private static class AnimatableTransition extends Transition { 236 private final Animatable mA; 237 238 public AnimatableTransition(Animatable a) { 239 mA = a; 240 } 241 242 @Override 243 public void start() { 244 mA.start(); 245 } 246 247 @Override 248 public void stop() { 249 mA.stop(); 250 } 251 } 252 253 254 private static class AnimationDrawableTransition extends Transition { 255 private final ObjectAnimator mAnim; 256 257 public AnimationDrawableTransition(AnimationDrawable ad, boolean reversed) { 258 final int frameCount = ad.getNumberOfFrames(); 259 final int fromFrame = reversed ? frameCount - 1 : 0; 260 final int toFrame = reversed ? 0 : frameCount - 1; 261 final FrameInterpolator interp = new FrameInterpolator(ad, reversed); 262 final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame); 263 anim.setAutoCancel(true); 264 anim.setDuration(interp.getTotalDuration()); 265 anim.setInterpolator(interp); 266 267 mAnim = anim; 268 } 269 270 @Override 271 public boolean canReverse() { 272 return true; 273 } 274 275 @Override 276 public void start() { 277 mAnim.start(); 278 } 279 280 @Override 281 public void reverse() { 282 mAnim.reverse(); 283 } 284 285 @Override 286 public void stop() { 287 mAnim.cancel(); 288 } 289 } 290 291 private static class AnimatedVectorDrawableTransition extends Transition { 292 private final AnimatedVectorDrawable mAvd; 293 private final boolean mReversed; 294 295 public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd, boolean reversed) { 296 mAvd = avd; 297 mReversed = reversed; 298 } 299 300 @Override 301 public boolean canReverse() { 302 return mAvd.canReverse(); 303 } 304 305 @Override 306 public void start() { 307 if (mReversed) { 308 reverse(); 309 } else { 310 mAvd.start(); 311 } 312 } 313 314 @Override 315 public void reverse() { 316 if (canReverse()) { 317 mAvd.reverse(); 318 } else { 319 Log.w(LOGTAG, "Reverse() is called on a drawable can't reverse"); 320 } 321 } 322 323 @Override 324 public void stop() { 325 mAvd.stop(); 326 } 327 } 328 329 330 @Override 331 public void jumpToCurrentState() { 332 super.jumpToCurrentState(); 333 334 if (mTransition != null) { 335 mTransition.stop(); 336 mTransition = null; 337 338 selectDrawable(mTransitionToIndex); 339 mTransitionToIndex = -1; 340 mTransitionFromIndex = -1; 341 } 342 } 343 344 @Override 345 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 346 @NonNull AttributeSet attrs, @Nullable Theme theme) 347 throws XmlPullParserException, IOException { 348 final TypedArray a = obtainAttributes( 349 r, theme, attrs, R.styleable.AnimatedStateListDrawable); 350 super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible); 351 updateStateFromTypedArray(a); 352 a.recycle(); 353 354 inflateChildElements(r, parser, attrs, theme); 355 356 init(); 357 } 358 359 @Override 360 public void applyTheme(@Nullable Theme theme) { 361 super.applyTheme(theme); 362 363 final AnimatedStateListState state = mState; 364 if (state == null || state.mAnimThemeAttrs == null) { 365 return; 366 } 367 368 final TypedArray a = theme.resolveAttributes( 369 state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable); 370 updateStateFromTypedArray(a); 371 a.recycle(); 372 373 init(); 374 } 375 376 private void updateStateFromTypedArray(TypedArray a) { 377 final AnimatedStateListState state = mState; 378 379 // Account for any configuration changes. 380 state.mChangingConfigurations |= a.getChangingConfigurations(); 381 382 // Extract the theme attributes, if any. 383 state.mAnimThemeAttrs = a.extractThemeAttrs(); 384 385 state.setVariablePadding(a.getBoolean( 386 R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding)); 387 state.setConstantSize(a.getBoolean( 388 R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize)); 389 state.setEnterFadeDuration(a.getInt( 390 R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration)); 391 state.setExitFadeDuration(a.getInt( 392 R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration)); 393 394 setDither(a.getBoolean( 395 R.styleable.AnimatedStateListDrawable_dither, state.mDither)); 396 setAutoMirrored(a.getBoolean( 397 R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored)); 398 } 399 400 private void init() { 401 onStateChange(getState()); 402 } 403 404 private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, 405 Theme theme) throws XmlPullParserException, IOException { 406 int type; 407 408 final int innerDepth = parser.getDepth() + 1; 409 int depth; 410 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 411 && ((depth = parser.getDepth()) >= innerDepth 412 || type != XmlPullParser.END_TAG)) { 413 if (type != XmlPullParser.START_TAG) { 414 continue; 415 } 416 417 if (depth > innerDepth) { 418 continue; 419 } 420 421 if (parser.getName().equals(ELEMENT_ITEM)) { 422 parseItem(r, parser, attrs, theme); 423 } else if (parser.getName().equals(ELEMENT_TRANSITION)) { 424 parseTransition(r, parser, attrs, theme); 425 } 426 } 427 } 428 429 private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser, 430 @NonNull AttributeSet attrs, @Nullable Theme theme) 431 throws XmlPullParserException, IOException { 432 // This allows state list drawable item elements to be themed at 433 // inflation time but does NOT make them work for Zygote preload. 434 final TypedArray a = obtainAttributes(r, theme, attrs, 435 R.styleable.AnimatedStateListDrawableTransition); 436 final int fromId = a.getResourceId( 437 R.styleable.AnimatedStateListDrawableTransition_fromId, 0); 438 final int toId = a.getResourceId( 439 R.styleable.AnimatedStateListDrawableTransition_toId, 0); 440 final boolean reversible = a.getBoolean( 441 R.styleable.AnimatedStateListDrawableTransition_reversible, false); 442 Drawable dr = a.getDrawable( 443 R.styleable.AnimatedStateListDrawableTransition_drawable); 444 a.recycle(); 445 446 // Loading child elements modifies the state of the AttributeSet's 447 // underlying parser, so it needs to happen after obtaining 448 // attributes and extracting states. 449 if (dr == null) { 450 int type; 451 while ((type = parser.next()) == XmlPullParser.TEXT) { 452 } 453 if (type != XmlPullParser.START_TAG) { 454 throw new XmlPullParserException( 455 parser.getPositionDescription() 456 + ": <transition> tag requires a 'drawable' attribute or " 457 + "child tag defining a drawable"); 458 } 459 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 460 } 461 462 return mState.addTransition(fromId, toId, dr, reversible); 463 } 464 465 private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser, 466 @NonNull AttributeSet attrs, @Nullable Theme theme) 467 throws XmlPullParserException, IOException { 468 // This allows state list drawable item elements to be themed at 469 // inflation time but does NOT make them work for Zygote preload. 470 final TypedArray a = obtainAttributes(r, theme, attrs, 471 R.styleable.AnimatedStateListDrawableItem); 472 final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0); 473 Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable); 474 a.recycle(); 475 476 final int[] states = extractStateSet(attrs); 477 478 // Loading child elements modifies the state of the AttributeSet's 479 // underlying parser, so it needs to happen after obtaining 480 // attributes and extracting states. 481 if (dr == null) { 482 int type; 483 while ((type = parser.next()) == XmlPullParser.TEXT) { 484 } 485 if (type != XmlPullParser.START_TAG) { 486 throw new XmlPullParserException( 487 parser.getPositionDescription() 488 + ": <item> tag requires a 'drawable' attribute or " 489 + "child tag defining a drawable"); 490 } 491 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 492 } 493 494 return mState.addStateSet(states, dr, keyframeId); 495 } 496 497 @Override 498 public Drawable mutate() { 499 if (!mMutated && super.mutate() == this) { 500 final AnimatedStateListState newState = new AnimatedStateListState(mState, this, null); 501 setConstantState(newState); 502 mMutated = true; 503 } 504 505 return this; 506 } 507 508 /** 509 * @hide 510 */ 511 public void clearMutated() { 512 super.clearMutated(); 513 mMutated = false; 514 } 515 516 static class AnimatedStateListState extends StateListState { 517 private static final int REVERSE_SHIFT = 32; 518 private static final int REVERSE_MASK = 0x1; 519 520 int[] mAnimThemeAttrs; 521 522 final LongSparseLongArray mTransitions; 523 final SparseIntArray mStateIds; 524 525 AnimatedStateListState(@Nullable AnimatedStateListState orig, 526 @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) { 527 super(orig, owner, res); 528 529 if (orig != null) { 530 mAnimThemeAttrs = orig.mAnimThemeAttrs; 531 mTransitions = orig.mTransitions.clone(); 532 mStateIds = orig.mStateIds.clone(); 533 } else { 534 mTransitions = new LongSparseLongArray(); 535 mStateIds = new SparseIntArray(); 536 } 537 } 538 539 int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) { 540 final int pos = super.addChild(anim); 541 final long keyFromTo = generateTransitionKey(fromId, toId); 542 mTransitions.append(keyFromTo, pos); 543 544 if (reversible) { 545 final long keyToFrom = generateTransitionKey(toId, fromId); 546 mTransitions.append(keyToFrom, pos | (1L << REVERSE_SHIFT)); 547 } 548 549 return addChild(anim); 550 } 551 552 int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { 553 final int index = super.addStateSet(stateSet, drawable); 554 mStateIds.put(index, id); 555 return index; 556 } 557 558 int indexOfKeyframe(@NonNull int[] stateSet) { 559 final int index = super.indexOfStateSet(stateSet); 560 if (index >= 0) { 561 return index; 562 } 563 564 return super.indexOfStateSet(StateSet.WILD_CARD); 565 } 566 567 int getKeyframeIdAt(int index) { 568 return index < 0 ? 0 : mStateIds.get(index, 0); 569 } 570 571 int indexOfTransition(int fromId, int toId) { 572 final long keyFromTo = generateTransitionKey(fromId, toId); 573 return (int) mTransitions.get(keyFromTo, -1); 574 } 575 576 boolean isTransitionReversed(int fromId, int toId) { 577 final long keyFromTo = generateTransitionKey(fromId, toId); 578 return (mTransitions.get(keyFromTo, -1) >> REVERSE_SHIFT & REVERSE_MASK) == 1; 579 } 580 581 @Override 582 public boolean canApplyTheme() { 583 return mAnimThemeAttrs != null || super.canApplyTheme(); 584 } 585 586 @Override 587 public Drawable newDrawable() { 588 return new AnimatedStateListDrawable(this, null); 589 } 590 591 @Override 592 public Drawable newDrawable(Resources res) { 593 return new AnimatedStateListDrawable(this, res); 594 } 595 596 private static long generateTransitionKey(int fromId, int toId) { 597 return (long) fromId << 32 | toId; 598 } 599 } 600 601 void setConstantState(@NonNull AnimatedStateListState state) { 602 super.setConstantState(state); 603 604 mState = state; 605 } 606 607 private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) { 608 super(null); 609 610 final AnimatedStateListState newState = new AnimatedStateListState(state, this, res); 611 setConstantState(newState); 612 onStateChange(getState()); 613 jumpToCurrentState(); 614 } 615 616 /** 617 * Interpolates between frames with respect to their individual durations. 618 */ 619 private static class FrameInterpolator implements TimeInterpolator { 620 private int[] mFrameTimes; 621 private int mFrames; 622 private int mTotalDuration; 623 624 public FrameInterpolator(AnimationDrawable d, boolean reversed) { 625 updateFrames(d, reversed); 626 } 627 628 public int updateFrames(AnimationDrawable d, boolean reversed) { 629 final int N = d.getNumberOfFrames(); 630 mFrames = N; 631 632 if (mFrameTimes == null || mFrameTimes.length < N) { 633 mFrameTimes = new int[N]; 634 } 635 636 final int[] frameTimes = mFrameTimes; 637 int totalDuration = 0; 638 for (int i = 0; i < N; i++) { 639 final int duration = d.getDuration(reversed ? N - i - 1 : i); 640 frameTimes[i] = duration; 641 totalDuration += duration; 642 } 643 644 mTotalDuration = totalDuration; 645 return totalDuration; 646 } 647 648 public int getTotalDuration() { 649 return mTotalDuration; 650 } 651 652 @Override 653 public float getInterpolation(float input) { 654 final int elapsed = (int) (input * mTotalDuration + 0.5f); 655 final int N = mFrames; 656 final int[] frameTimes = mFrameTimes; 657 658 // Find the current frame and remaining time within that frame. 659 int remaining = elapsed; 660 int i = 0; 661 while (i < N && remaining >= frameTimes[i]) { 662 remaining -= frameTimes[i]; 663 i++; 664 } 665 666 // Remaining time is relative of total duration. 667 final float frameElapsed; 668 if (i < N) { 669 frameElapsed = remaining / (float) mTotalDuration; 670 } else { 671 frameElapsed = 0; 672 } 673 674 return i / (float) N + frameElapsed; 675 } 676 } 677} 678