• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         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