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