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  * &lt;animated-selector></code> element. Each keyframe Drawable is defined in a
48  * nested <code>&lt;item></code> element. Transitions are defined in a nested
49  * <code>&lt;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