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  * &lt;animated-selector></code> element. Each keyframe Drawable is defined in a
46  * nested <code>&lt;item></code> element. Transitions are defined in a nested
47  * <code>&lt;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         a.recycle();
383 
384         inflateChildElements(r, parser, attrs, theme);
385 
386         init();
387     }
388 
389     @Override
applyTheme(@ullable Theme theme)390     public void applyTheme(@Nullable Theme theme) {
391         super.applyTheme(theme);
392 
393         final AnimatedStateListState state = mState;
394         if (state == null || state.mAnimThemeAttrs == null) {
395             return;
396         }
397 
398         final TypedArray a = theme.resolveAttributes(
399                 state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable);
400         updateStateFromTypedArray(a);
401         a.recycle();
402 
403         init();
404     }
405 
updateStateFromTypedArray(TypedArray a)406     private void updateStateFromTypedArray(TypedArray a) {
407         final AnimatedStateListState state = mState;
408 
409         // Account for any configuration changes.
410         state.mChangingConfigurations |= a.getChangingConfigurations();
411 
412         // Extract the theme attributes, if any.
413         state.mAnimThemeAttrs = a.extractThemeAttrs();
414 
415         state.setVariablePadding(a.getBoolean(
416                 R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding));
417         state.setConstantSize(a.getBoolean(
418                 R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize));
419         state.setEnterFadeDuration(a.getInt(
420                 R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration));
421         state.setExitFadeDuration(a.getInt(
422                 R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration));
423 
424         setDither(a.getBoolean(
425                 R.styleable.AnimatedStateListDrawable_dither, state.mDither));
426         setAutoMirrored(a.getBoolean(
427                 R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored));
428     }
429 
init()430     private void init() {
431         onStateChange(getState());
432     }
433 
inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)434     private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
435             Theme theme) throws XmlPullParserException, IOException {
436         int type;
437 
438         final int innerDepth = parser.getDepth() + 1;
439         int depth;
440         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
441                 && ((depth = parser.getDepth()) >= innerDepth
442                 || type != XmlPullParser.END_TAG)) {
443             if (type != XmlPullParser.START_TAG) {
444                 continue;
445             }
446 
447             if (depth > innerDepth) {
448                 continue;
449             }
450 
451             if (parser.getName().equals(ELEMENT_ITEM)) {
452                 parseItem(r, parser, attrs, theme);
453             } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
454                 parseTransition(r, parser, attrs, theme);
455             }
456         }
457     }
458 
parseTransition(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)459     private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser,
460             @NonNull AttributeSet attrs, @Nullable Theme theme)
461             throws XmlPullParserException, IOException {
462         // This allows state list drawable item elements to be themed at
463         // inflation time but does NOT make them work for Zygote preload.
464         final TypedArray a = obtainAttributes(r, theme, attrs,
465                 R.styleable.AnimatedStateListDrawableTransition);
466         final int fromId = a.getResourceId(
467                 R.styleable.AnimatedStateListDrawableTransition_fromId, 0);
468         final int toId = a.getResourceId(
469                 R.styleable.AnimatedStateListDrawableTransition_toId, 0);
470         final boolean reversible = a.getBoolean(
471                 R.styleable.AnimatedStateListDrawableTransition_reversible, false);
472         Drawable dr = a.getDrawable(
473                 R.styleable.AnimatedStateListDrawableTransition_drawable);
474         a.recycle();
475 
476         // Loading child elements modifies the state of the AttributeSet's
477         // underlying parser, so it needs to happen after obtaining
478         // attributes and extracting states.
479         if (dr == null) {
480             int type;
481             while ((type = parser.next()) == XmlPullParser.TEXT) {
482             }
483             if (type != XmlPullParser.START_TAG) {
484                 throw new XmlPullParserException(
485                         parser.getPositionDescription()
486                                 + ": <transition> tag requires a 'drawable' attribute or "
487                                 + "child tag defining a drawable");
488             }
489             dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
490         }
491 
492         return mState.addTransition(fromId, toId, dr, reversible);
493     }
494 
parseItem(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)495     private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser,
496             @NonNull AttributeSet attrs, @Nullable Theme theme)
497             throws XmlPullParserException, IOException {
498         // This allows state list drawable item elements to be themed at
499         // inflation time but does NOT make them work for Zygote preload.
500         final TypedArray a = obtainAttributes(r, theme, attrs,
501                 R.styleable.AnimatedStateListDrawableItem);
502         final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0);
503         Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable);
504         a.recycle();
505 
506         final int[] states = extractStateSet(attrs);
507 
508         // Loading child elements modifies the state of the AttributeSet's
509         // underlying parser, so it needs to happen after obtaining
510         // attributes and extracting states.
511         if (dr == null) {
512             int type;
513             while ((type = parser.next()) == XmlPullParser.TEXT) {
514             }
515             if (type != XmlPullParser.START_TAG) {
516                 throw new XmlPullParserException(
517                         parser.getPositionDescription()
518                                 + ": <item> tag requires a 'drawable' attribute or "
519                                 + "child tag defining a drawable");
520             }
521             dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
522         }
523 
524         return mState.addStateSet(states, dr, keyframeId);
525     }
526 
527     @Override
mutate()528     public Drawable mutate() {
529         if (!mMutated && super.mutate() == this) {
530             mState.mutate();
531             mMutated = true;
532         }
533 
534         return this;
535     }
536 
537     @Override
cloneConstantState()538     AnimatedStateListState cloneConstantState() {
539         return new AnimatedStateListState(mState, this, null);
540     }
541 
542     /**
543      * @hide
544      */
clearMutated()545     public void clearMutated() {
546         super.clearMutated();
547         mMutated = false;
548     }
549 
550     static class AnimatedStateListState extends StateListState {
551         // REVERSED_BIT is indicating the current transition's direction.
552         private static final long REVERSED_BIT = 0x100000000l;
553 
554         // REVERSIBLE_FLAG_BIT is indicating whether the whole transition has
555         // reversible flag set to true.
556         private static final long REVERSIBLE_FLAG_BIT = 0x200000000l;
557 
558         int[] mAnimThemeAttrs;
559 
560         LongSparseLongArray mTransitions;
561         SparseIntArray mStateIds;
562 
AnimatedStateListState(@ullable AnimatedStateListState orig, @NonNull AnimatedStateListDrawable owner, @Nullable Resources res)563         AnimatedStateListState(@Nullable AnimatedStateListState orig,
564                 @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) {
565             super(orig, owner, res);
566 
567             if (orig != null) {
568                 // Perform a shallow copy and rely on mutate() to deep-copy.
569                 mAnimThemeAttrs = orig.mAnimThemeAttrs;
570                 mTransitions = orig.mTransitions;
571                 mStateIds = orig.mStateIds;
572             } else {
573                 mTransitions = new LongSparseLongArray();
574                 mStateIds = new SparseIntArray();
575             }
576         }
577 
mutate()578         void mutate() {
579             mTransitions = mTransitions.clone();
580             mStateIds = mStateIds.clone();
581         }
582 
addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible)583         int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) {
584             final int pos = super.addChild(anim);
585             final long keyFromTo = generateTransitionKey(fromId, toId);
586             long reversibleBit = 0;
587             if (reversible) {
588                 reversibleBit = REVERSIBLE_FLAG_BIT;
589             }
590             mTransitions.append(keyFromTo, pos | reversibleBit);
591 
592             if (reversible) {
593                 final long keyToFrom = generateTransitionKey(toId, fromId);
594                 mTransitions.append(keyToFrom, pos | REVERSED_BIT | reversibleBit);
595             }
596 
597             return pos;
598         }
599 
addStateSet(@onNull int[] stateSet, @NonNull Drawable drawable, int id)600         int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
601             final int index = super.addStateSet(stateSet, drawable);
602             mStateIds.put(index, id);
603             return index;
604         }
605 
indexOfKeyframe(@onNull int[] stateSet)606         int indexOfKeyframe(@NonNull int[] stateSet) {
607             final int index = super.indexOfStateSet(stateSet);
608             if (index >= 0) {
609                 return index;
610             }
611 
612             return super.indexOfStateSet(StateSet.WILD_CARD);
613         }
614 
getKeyframeIdAt(int index)615         int getKeyframeIdAt(int index) {
616             return index < 0 ? 0 : mStateIds.get(index, 0);
617         }
618 
indexOfTransition(int fromId, int toId)619         int indexOfTransition(int fromId, int toId) {
620             final long keyFromTo = generateTransitionKey(fromId, toId);
621             return (int) mTransitions.get(keyFromTo, -1);
622         }
623 
isTransitionReversed(int fromId, int toId)624         boolean isTransitionReversed(int fromId, int toId) {
625             final long keyFromTo = generateTransitionKey(fromId, toId);
626             return (mTransitions.get(keyFromTo, -1) & REVERSED_BIT) != 0;
627         }
628 
transitionHasReversibleFlag(int fromId, int toId)629         boolean transitionHasReversibleFlag(int fromId, int toId) {
630             final long keyFromTo = generateTransitionKey(fromId, toId);
631             return (mTransitions.get(keyFromTo, -1) & REVERSIBLE_FLAG_BIT) != 0;
632         }
633 
634         @Override
canApplyTheme()635         public boolean canApplyTheme() {
636             return mAnimThemeAttrs != null || super.canApplyTheme();
637         }
638 
639         @Override
newDrawable()640         public Drawable newDrawable() {
641             return new AnimatedStateListDrawable(this, null);
642         }
643 
644         @Override
newDrawable(Resources res)645         public Drawable newDrawable(Resources res) {
646             return new AnimatedStateListDrawable(this, res);
647         }
648 
generateTransitionKey(int fromId, int toId)649         private static long generateTransitionKey(int fromId, int toId) {
650             return (long) fromId << 32 | toId;
651         }
652     }
653 
654     @Override
setConstantState(@onNull DrawableContainerState state)655     protected void setConstantState(@NonNull DrawableContainerState state) {
656         super.setConstantState(state);
657 
658         if (state instanceof AnimatedStateListState) {
659             mState = (AnimatedStateListState) state;
660         }
661     }
662 
AnimatedStateListDrawable(@ullable AnimatedStateListState state, @Nullable Resources res)663     private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) {
664         super(null);
665 
666         // Every animated state list drawable has its own constant state.
667         final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
668         setConstantState(newState);
669         onStateChange(getState());
670         jumpToCurrentState();
671     }
672 
673     /**
674      * Interpolates between frames with respect to their individual durations.
675      */
676     private static class FrameInterpolator implements TimeInterpolator {
677         private int[] mFrameTimes;
678         private int mFrames;
679         private int mTotalDuration;
680 
FrameInterpolator(AnimationDrawable d, boolean reversed)681         public FrameInterpolator(AnimationDrawable d, boolean reversed) {
682             updateFrames(d, reversed);
683         }
684 
updateFrames(AnimationDrawable d, boolean reversed)685         public int updateFrames(AnimationDrawable d, boolean reversed) {
686             final int N = d.getNumberOfFrames();
687             mFrames = N;
688 
689             if (mFrameTimes == null || mFrameTimes.length < N) {
690                 mFrameTimes = new int[N];
691             }
692 
693             final int[] frameTimes = mFrameTimes;
694             int totalDuration = 0;
695             for (int i = 0; i < N; i++) {
696                 final int duration = d.getDuration(reversed ? N - i - 1 : i);
697                 frameTimes[i] = duration;
698                 totalDuration += duration;
699             }
700 
701             mTotalDuration = totalDuration;
702             return totalDuration;
703         }
704 
getTotalDuration()705         public int getTotalDuration() {
706             return mTotalDuration;
707         }
708 
709         @Override
getInterpolation(float input)710         public float getInterpolation(float input) {
711             final int elapsed = (int) (input * mTotalDuration + 0.5f);
712             final int N = mFrames;
713             final int[] frameTimes = mFrameTimes;
714 
715             // Find the current frame and remaining time within that frame.
716             int remaining = elapsed;
717             int i = 0;
718             while (i < N && remaining >= frameTimes[i]) {
719                 remaining -= frameTimes[i];
720                 i++;
721             }
722 
723             // Remaining time is relative of total duration.
724             final float frameElapsed;
725             if (i < N) {
726                 frameElapsed = remaining / (float) mTotalDuration;
727             } else {
728                 frameElapsed = 0;
729             }
730 
731             return i / (float) N + frameElapsed;
732         }
733     }
734 }
735