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 17 package android.graphics.drawable; 18 19 import android.animation.ObjectAnimator; 20 import android.animation.TimeInterpolator; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.res.Resources; 24 import android.content.res.Resources.Theme; 25 import android.content.res.TypedArray; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.util.LongSparseLongArray; 29 import android.util.SparseIntArray; 30 import android.util.StateSet; 31 32 import com.android.internal.R; 33 34 import org.xmlpull.v1.XmlPullParser; 35 import org.xmlpull.v1.XmlPullParserException; 36 37 import 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 */ 63 public 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 AnimatedStateListDrawable()82 public AnimatedStateListDrawable() { 83 this(null, null); 84 } 85 86 @Override setVisible(boolean visible, boolean restart)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 */ addState(@onNull int[] stateSet, @NonNull Drawable drawable, int id)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 */ addTransition(int fromId, int toId, @NonNull T transition, boolean reversible)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 isStateful()136 public boolean isStateful() { 137 return true; 138 } 139 140 @Override onStateChange(int[] stateSet)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 boolean changed = targetIndex != getCurrentIndex() 146 && (selectTransition(targetIndex) || selectDrawable(targetIndex)); 147 148 // We need to propagate the state change to the current drawable, but 149 // we can't call StateListDrawable.onStateChange() without changing the 150 // current drawable. 151 final Drawable current = getCurrent(); 152 if (current != null) { 153 changed |= current.setState(stateSet); 154 } 155 156 return changed; 157 } 158 selectTransition(int toIndex)159 private boolean selectTransition(int toIndex) { 160 final int fromIndex; 161 final Transition currentTransition = mTransition; 162 if (currentTransition != null) { 163 if (toIndex == mTransitionToIndex) { 164 // Already animating to that keyframe. 165 return true; 166 } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) { 167 // Reverse the current animation. 168 currentTransition.reverse(); 169 mTransitionToIndex = mTransitionFromIndex; 170 mTransitionFromIndex = toIndex; 171 return true; 172 } 173 174 // Start the next transition from the end of the current one. 175 fromIndex = mTransitionToIndex; 176 177 // Changing animation, end the current animation. 178 currentTransition.stop(); 179 } else { 180 fromIndex = getCurrentIndex(); 181 } 182 183 // Reset state. 184 mTransition = null; 185 mTransitionFromIndex = -1; 186 mTransitionToIndex = -1; 187 188 final AnimatedStateListState state = mState; 189 final int fromId = state.getKeyframeIdAt(fromIndex); 190 final int toId = state.getKeyframeIdAt(toIndex); 191 if (toId == 0 || fromId == 0) { 192 // Missing a keyframe ID. 193 return false; 194 } 195 196 final int transitionIndex = state.indexOfTransition(fromId, toId); 197 if (transitionIndex < 0) { 198 // Couldn't select a transition. 199 return false; 200 } 201 202 boolean hasReversibleFlag = state.transitionHasReversibleFlag(fromId, toId); 203 204 // This may fail if we're already on the transition, but that's okay! 205 selectDrawable(transitionIndex); 206 207 final Transition transition; 208 final Drawable d = getCurrent(); 209 if (d instanceof AnimationDrawable) { 210 final boolean reversed = state.isTransitionReversed(fromId, toId); 211 212 transition = new AnimationDrawableTransition((AnimationDrawable) d, 213 reversed, hasReversibleFlag); 214 } else if (d instanceof AnimatedVectorDrawable) { 215 final boolean reversed = state.isTransitionReversed(fromId, toId); 216 217 transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d, 218 reversed, hasReversibleFlag); 219 } else if (d instanceof Animatable) { 220 transition = new AnimatableTransition((Animatable) d); 221 } else { 222 // We don't know how to animate this transition. 223 return false; 224 } 225 226 transition.start(); 227 228 mTransition = transition; 229 mTransitionFromIndex = fromIndex; 230 mTransitionToIndex = toIndex; 231 return true; 232 } 233 234 private static abstract class Transition { start()235 public abstract void start(); stop()236 public abstract void stop(); 237 reverse()238 public void reverse() { 239 // Not supported by default. 240 } 241 canReverse()242 public boolean canReverse() { 243 return false; 244 } 245 } 246 247 private static class AnimatableTransition extends Transition { 248 private final Animatable mA; 249 AnimatableTransition(Animatable a)250 public AnimatableTransition(Animatable a) { 251 mA = a; 252 } 253 254 @Override start()255 public void start() { 256 mA.start(); 257 } 258 259 @Override stop()260 public void stop() { 261 mA.stop(); 262 } 263 } 264 265 266 private static class AnimationDrawableTransition extends Transition { 267 private final ObjectAnimator mAnim; 268 269 // Even AnimationDrawable is always reversible technically, but 270 // we should obey the XML's android:reversible flag. 271 private final boolean mHasReversibleFlag; 272 AnimationDrawableTransition(AnimationDrawable ad, boolean reversed, boolean hasReversibleFlag)273 public AnimationDrawableTransition(AnimationDrawable ad, 274 boolean reversed, boolean hasReversibleFlag) { 275 final int frameCount = ad.getNumberOfFrames(); 276 final int fromFrame = reversed ? frameCount - 1 : 0; 277 final int toFrame = reversed ? 0 : frameCount - 1; 278 final FrameInterpolator interp = new FrameInterpolator(ad, reversed); 279 final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame); 280 anim.setAutoCancel(true); 281 anim.setDuration(interp.getTotalDuration()); 282 anim.setInterpolator(interp); 283 mHasReversibleFlag = hasReversibleFlag; 284 mAnim = anim; 285 } 286 287 @Override canReverse()288 public boolean canReverse() { 289 return mHasReversibleFlag; 290 } 291 292 @Override start()293 public void start() { 294 mAnim.start(); 295 } 296 297 @Override reverse()298 public void reverse() { 299 mAnim.reverse(); 300 } 301 302 @Override stop()303 public void stop() { 304 mAnim.cancel(); 305 } 306 } 307 308 private static class AnimatedVectorDrawableTransition extends Transition { 309 private final AnimatedVectorDrawable mAvd; 310 311 // mReversed is indicating the current transition's direction. 312 private final boolean mReversed; 313 314 // mHasReversibleFlag is indicating whether the whole transition has 315 // reversible flag set to true. 316 // If mHasReversibleFlag is false, then mReversed is always false. 317 private final boolean mHasReversibleFlag; 318 AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd, boolean reversed, boolean hasReversibleFlag)319 public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd, 320 boolean reversed, boolean hasReversibleFlag) { 321 mAvd = avd; 322 mReversed = reversed; 323 mHasReversibleFlag = hasReversibleFlag; 324 } 325 326 @Override canReverse()327 public boolean canReverse() { 328 // When the transition's XML says it is not reversible, then we obey 329 // it, even if the AVD itself is reversible. 330 // This will help the single direction transition. 331 return mAvd.canReverse() && mHasReversibleFlag; 332 } 333 334 @Override start()335 public void start() { 336 if (mReversed) { 337 reverse(); 338 } else { 339 mAvd.start(); 340 } 341 } 342 343 @Override reverse()344 public void reverse() { 345 if (canReverse()) { 346 mAvd.reverse(); 347 } else { 348 Log.w(LOGTAG, "Can't reverse, either the reversible is set to false," 349 + " or the AnimatedVectorDrawable can't reverse"); 350 } 351 } 352 353 @Override stop()354 public void stop() { 355 mAvd.stop(); 356 } 357 } 358 359 360 @Override jumpToCurrentState()361 public void jumpToCurrentState() { 362 super.jumpToCurrentState(); 363 364 if (mTransition != null) { 365 mTransition.stop(); 366 mTransition = null; 367 368 selectDrawable(mTransitionToIndex); 369 mTransitionToIndex = -1; 370 mTransitionFromIndex = -1; 371 } 372 } 373 374 @Override inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)375 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 376 @NonNull AttributeSet attrs, @Nullable Theme theme) 377 throws XmlPullParserException, IOException { 378 final TypedArray a = obtainAttributes( 379 r, theme, attrs, R.styleable.AnimatedStateListDrawable); 380 super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible); 381 updateStateFromTypedArray(a); 382 updateDensity(r); 383 a.recycle(); 384 385 inflateChildElements(r, parser, attrs, theme); 386 387 init(); 388 } 389 390 @Override applyTheme(@ullable Theme theme)391 public void applyTheme(@Nullable Theme theme) { 392 super.applyTheme(theme); 393 394 final AnimatedStateListState state = mState; 395 if (state == null || state.mAnimThemeAttrs == null) { 396 return; 397 } 398 399 final TypedArray a = theme.resolveAttributes( 400 state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable); 401 updateStateFromTypedArray(a); 402 a.recycle(); 403 404 init(); 405 } 406 updateStateFromTypedArray(TypedArray a)407 private void updateStateFromTypedArray(TypedArray a) { 408 final AnimatedStateListState state = mState; 409 410 // Account for any configuration changes. 411 state.mChangingConfigurations |= a.getChangingConfigurations(); 412 413 // Extract the theme attributes, if any. 414 state.mAnimThemeAttrs = a.extractThemeAttrs(); 415 416 state.setVariablePadding(a.getBoolean( 417 R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding)); 418 state.setConstantSize(a.getBoolean( 419 R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize)); 420 state.setEnterFadeDuration(a.getInt( 421 R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration)); 422 state.setExitFadeDuration(a.getInt( 423 R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration)); 424 425 setDither(a.getBoolean( 426 R.styleable.AnimatedStateListDrawable_dither, state.mDither)); 427 setAutoMirrored(a.getBoolean( 428 R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored)); 429 } 430 init()431 private void init() { 432 onStateChange(getState()); 433 } 434 inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)435 private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, 436 Theme theme) throws XmlPullParserException, IOException { 437 int type; 438 439 final int innerDepth = parser.getDepth() + 1; 440 int depth; 441 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 442 && ((depth = parser.getDepth()) >= innerDepth 443 || type != XmlPullParser.END_TAG)) { 444 if (type != XmlPullParser.START_TAG) { 445 continue; 446 } 447 448 if (depth > innerDepth) { 449 continue; 450 } 451 452 if (parser.getName().equals(ELEMENT_ITEM)) { 453 parseItem(r, parser, attrs, theme); 454 } else if (parser.getName().equals(ELEMENT_TRANSITION)) { 455 parseTransition(r, parser, attrs, theme); 456 } 457 } 458 } 459 parseTransition(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)460 private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser, 461 @NonNull AttributeSet attrs, @Nullable Theme theme) 462 throws XmlPullParserException, IOException { 463 // This allows state list drawable item elements to be themed at 464 // inflation time but does NOT make them work for Zygote preload. 465 final TypedArray a = obtainAttributes(r, theme, attrs, 466 R.styleable.AnimatedStateListDrawableTransition); 467 final int fromId = a.getResourceId( 468 R.styleable.AnimatedStateListDrawableTransition_fromId, 0); 469 final int toId = a.getResourceId( 470 R.styleable.AnimatedStateListDrawableTransition_toId, 0); 471 final boolean reversible = a.getBoolean( 472 R.styleable.AnimatedStateListDrawableTransition_reversible, false); 473 Drawable dr = a.getDrawable( 474 R.styleable.AnimatedStateListDrawableTransition_drawable); 475 a.recycle(); 476 477 // Loading child elements modifies the state of the AttributeSet's 478 // underlying parser, so it needs to happen after obtaining 479 // attributes and extracting states. 480 if (dr == null) { 481 int type; 482 while ((type = parser.next()) == XmlPullParser.TEXT) { 483 } 484 if (type != XmlPullParser.START_TAG) { 485 throw new XmlPullParserException( 486 parser.getPositionDescription() 487 + ": <transition> tag requires a 'drawable' attribute or " 488 + "child tag defining a drawable"); 489 } 490 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 491 } 492 493 return mState.addTransition(fromId, toId, dr, reversible); 494 } 495 parseItem(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)496 private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser, 497 @NonNull AttributeSet attrs, @Nullable Theme theme) 498 throws XmlPullParserException, IOException { 499 // This allows state list drawable item elements to be themed at 500 // inflation time but does NOT make them work for Zygote preload. 501 final TypedArray a = obtainAttributes(r, theme, attrs, 502 R.styleable.AnimatedStateListDrawableItem); 503 final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0); 504 Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable); 505 a.recycle(); 506 507 final int[] states = extractStateSet(attrs); 508 509 // Loading child elements modifies the state of the AttributeSet's 510 // underlying parser, so it needs to happen after obtaining 511 // attributes and extracting states. 512 if (dr == null) { 513 int type; 514 while ((type = parser.next()) == XmlPullParser.TEXT) { 515 } 516 if (type != XmlPullParser.START_TAG) { 517 throw new XmlPullParserException( 518 parser.getPositionDescription() 519 + ": <item> tag requires a 'drawable' attribute or " 520 + "child tag defining a drawable"); 521 } 522 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 523 } 524 525 return mState.addStateSet(states, dr, keyframeId); 526 } 527 528 @Override mutate()529 public Drawable mutate() { 530 if (!mMutated && super.mutate() == this) { 531 mState.mutate(); 532 mMutated = true; 533 } 534 535 return this; 536 } 537 538 @Override cloneConstantState()539 AnimatedStateListState cloneConstantState() { 540 return new AnimatedStateListState(mState, this, null); 541 } 542 543 /** 544 * @hide 545 */ clearMutated()546 public void clearMutated() { 547 super.clearMutated(); 548 mMutated = false; 549 } 550 551 static class AnimatedStateListState extends StateListState { 552 // REVERSED_BIT is indicating the current transition's direction. 553 private static final long REVERSED_BIT = 0x100000000l; 554 555 // REVERSIBLE_FLAG_BIT is indicating whether the whole transition has 556 // reversible flag set to true. 557 private static final long REVERSIBLE_FLAG_BIT = 0x200000000l; 558 559 int[] mAnimThemeAttrs; 560 561 LongSparseLongArray mTransitions; 562 SparseIntArray mStateIds; 563 AnimatedStateListState(@ullable AnimatedStateListState orig, @NonNull AnimatedStateListDrawable owner, @Nullable Resources res)564 AnimatedStateListState(@Nullable AnimatedStateListState orig, 565 @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) { 566 super(orig, owner, res); 567 568 if (orig != null) { 569 // Perform a shallow copy and rely on mutate() to deep-copy. 570 mAnimThemeAttrs = orig.mAnimThemeAttrs; 571 mTransitions = orig.mTransitions; 572 mStateIds = orig.mStateIds; 573 } else { 574 mTransitions = new LongSparseLongArray(); 575 mStateIds = new SparseIntArray(); 576 } 577 } 578 mutate()579 void mutate() { 580 mTransitions = mTransitions.clone(); 581 mStateIds = mStateIds.clone(); 582 } 583 addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible)584 int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) { 585 final int pos = super.addChild(anim); 586 final long keyFromTo = generateTransitionKey(fromId, toId); 587 long reversibleBit = 0; 588 if (reversible) { 589 reversibleBit = REVERSIBLE_FLAG_BIT; 590 } 591 mTransitions.append(keyFromTo, pos | reversibleBit); 592 593 if (reversible) { 594 final long keyToFrom = generateTransitionKey(toId, fromId); 595 mTransitions.append(keyToFrom, pos | REVERSED_BIT | reversibleBit); 596 } 597 598 return pos; 599 } 600 addStateSet(@onNull int[] stateSet, @NonNull Drawable drawable, int id)601 int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { 602 final int index = super.addStateSet(stateSet, drawable); 603 mStateIds.put(index, id); 604 return index; 605 } 606 indexOfKeyframe(@onNull int[] stateSet)607 int indexOfKeyframe(@NonNull int[] stateSet) { 608 final int index = super.indexOfStateSet(stateSet); 609 if (index >= 0) { 610 return index; 611 } 612 613 return super.indexOfStateSet(StateSet.WILD_CARD); 614 } 615 getKeyframeIdAt(int index)616 int getKeyframeIdAt(int index) { 617 return index < 0 ? 0 : mStateIds.get(index, 0); 618 } 619 indexOfTransition(int fromId, int toId)620 int indexOfTransition(int fromId, int toId) { 621 final long keyFromTo = generateTransitionKey(fromId, toId); 622 return (int) mTransitions.get(keyFromTo, -1); 623 } 624 isTransitionReversed(int fromId, int toId)625 boolean isTransitionReversed(int fromId, int toId) { 626 final long keyFromTo = generateTransitionKey(fromId, toId); 627 return (mTransitions.get(keyFromTo, -1) & REVERSED_BIT) != 0; 628 } 629 transitionHasReversibleFlag(int fromId, int toId)630 boolean transitionHasReversibleFlag(int fromId, int toId) { 631 final long keyFromTo = generateTransitionKey(fromId, toId); 632 return (mTransitions.get(keyFromTo, -1) & REVERSIBLE_FLAG_BIT) != 0; 633 } 634 635 @Override canApplyTheme()636 public boolean canApplyTheme() { 637 return mAnimThemeAttrs != null || super.canApplyTheme(); 638 } 639 640 @Override newDrawable()641 public Drawable newDrawable() { 642 return new AnimatedStateListDrawable(this, null); 643 } 644 645 @Override newDrawable(Resources res)646 public Drawable newDrawable(Resources res) { 647 return new AnimatedStateListDrawable(this, res); 648 } 649 generateTransitionKey(int fromId, int toId)650 private static long generateTransitionKey(int fromId, int toId) { 651 return (long) fromId << 32 | toId; 652 } 653 } 654 655 @Override setConstantState(@onNull DrawableContainerState state)656 protected void setConstantState(@NonNull DrawableContainerState state) { 657 super.setConstantState(state); 658 659 if (state instanceof AnimatedStateListState) { 660 mState = (AnimatedStateListState) state; 661 } 662 } 663 AnimatedStateListDrawable(@ullable AnimatedStateListState state, @Nullable Resources res)664 private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) { 665 super(null); 666 667 // Every animated state list drawable has its own constant state. 668 final AnimatedStateListState newState = new AnimatedStateListState(state, this, res); 669 setConstantState(newState); 670 onStateChange(getState()); 671 jumpToCurrentState(); 672 } 673 674 /** 675 * Interpolates between frames with respect to their individual durations. 676 */ 677 private static class FrameInterpolator implements TimeInterpolator { 678 private int[] mFrameTimes; 679 private int mFrames; 680 private int mTotalDuration; 681 FrameInterpolator(AnimationDrawable d, boolean reversed)682 public FrameInterpolator(AnimationDrawable d, boolean reversed) { 683 updateFrames(d, reversed); 684 } 685 updateFrames(AnimationDrawable d, boolean reversed)686 public int updateFrames(AnimationDrawable d, boolean reversed) { 687 final int N = d.getNumberOfFrames(); 688 mFrames = N; 689 690 if (mFrameTimes == null || mFrameTimes.length < N) { 691 mFrameTimes = new int[N]; 692 } 693 694 final int[] frameTimes = mFrameTimes; 695 int totalDuration = 0; 696 for (int i = 0; i < N; i++) { 697 final int duration = d.getDuration(reversed ? N - i - 1 : i); 698 frameTimes[i] = duration; 699 totalDuration += duration; 700 } 701 702 mTotalDuration = totalDuration; 703 return totalDuration; 704 } 705 getTotalDuration()706 public int getTotalDuration() { 707 return mTotalDuration; 708 } 709 710 @Override getInterpolation(float input)711 public float getInterpolation(float input) { 712 final int elapsed = (int) (input * mTotalDuration + 0.5f); 713 final int N = mFrames; 714 final int[] frameTimes = mFrameTimes; 715 716 // Find the current frame and remaining time within that frame. 717 int remaining = elapsed; 718 int i = 0; 719 while (i < N && remaining >= frameTimes[i]) { 720 remaining -= frameTimes[i]; 721 i++; 722 } 723 724 // Remaining time is relative of total duration. 725 final float frameElapsed; 726 if (i < N) { 727 frameElapsed = remaining / (float) mTotalDuration; 728 } else { 729 frameElapsed = 0; 730 } 731 732 return i / (float) N + frameElapsed; 733 } 734 } 735 } 736