summaryrefslogtreecommitdiffstats
path: root/graphics/java/android/graphics/drawable/AnimatedStateListDrawable.java
blob: e235a9983d5f2851ac806e491d7bee3273d27d97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.graphics.drawable;

import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.util.LongSparseLongArray;
import android.util.SparseIntArray;
import android.util.StateSet;

import com.android.internal.R;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;

/**
 * Drawable containing a set of Drawable keyframes where the currently displayed
 * keyframe is chosen based on the current state set. Animations between
 * keyframes may optionally be defined using transition elements.
 * <p>
 * This drawable can be defined in an XML file with the <code>
 * &lt;animated-selector></code> element. Each keyframe Drawable is defined in a
 * nested <code>&lt;item></code> element. Transitions are defined in a nested
 * <code>&lt;transition></code> element.
 *
 * @attr ref android.R.styleable#DrawableStates_state_focused
 * @attr ref android.R.styleable#DrawableStates_state_window_focused
 * @attr ref android.R.styleable#DrawableStates_state_enabled
 * @attr ref android.R.styleable#DrawableStates_state_checkable
 * @attr ref android.R.styleable#DrawableStates_state_checked
 * @attr ref android.R.styleable#DrawableStates_state_selected
 * @attr ref android.R.styleable#DrawableStates_state_activated
 * @attr ref android.R.styleable#DrawableStates_state_active
 * @attr ref android.R.styleable#DrawableStates_state_single
 * @attr ref android.R.styleable#DrawableStates_state_first
 * @attr ref android.R.styleable#DrawableStates_state_middle
 * @attr ref android.R.styleable#DrawableStates_state_last
 * @attr ref android.R.styleable#DrawableStates_state_pressed
 */
public class AnimatedStateListDrawable extends StateListDrawable {
    private static final String LOGTAG = AnimatedStateListDrawable.class.getSimpleName();

    private static final String ELEMENT_TRANSITION = "transition";
    private static final String ELEMENT_ITEM = "item";

    private AnimatedStateListState mState;

    /** The currently running transition, if any. */
    private Transition mTransition;

    /** Index to be set after the transition ends. */
    private int mTransitionToIndex = -1;

    /** Index away from which we are transitioning. */
    private int mTransitionFromIndex = -1;

    private boolean mMutated;

    public AnimatedStateListDrawable() {
        this(null, null);
    }

    @Override
    public boolean setVisible(boolean visible, boolean restart) {
        final boolean changed = super.setVisible(visible, restart);

        if (mTransition != null && (changed || restart)) {
            if (visible) {
                mTransition.start();
            } else {
                // Ensure we're showing the correct state when visible.
                jumpToCurrentState();
            }
        }

        return changed;
    }

    /**
     * Add a new drawable to the set of keyframes.
     *
     * @param stateSet An array of resource IDs to associate with the keyframe
     * @param drawable The drawable to show when in the specified state, may not be null
     * @param id The unique identifier for the keyframe
     */
    public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
        if (drawable == null) {
            throw new IllegalArgumentException("Drawable must not be null");
        }

        mState.addStateSet(stateSet, drawable, id);
        onStateChange(getState());
    }

    /**
     * Adds a new transition between keyframes.
     *
     * @param fromId Unique identifier of the starting keyframe
     * @param toId Unique identifier of the ending keyframe
     * @param transition An {@link Animatable} drawable to use as a transition, may not be null
     * @param reversible Whether the transition can be reversed
     */
    public <T extends Drawable & Animatable> void addTransition(int fromId, int toId,
            @NonNull T transition, boolean reversible) {
        if (transition == null) {
            throw new IllegalArgumentException("Transition drawable must not be null");
        }

        mState.addTransition(fromId, toId, transition, reversible);
    }

    @Override
    public boolean isStateful() {
        return true;
    }

    @Override
    protected boolean onStateChange(int[] stateSet) {
        // If we're not already at the target index, either attempt to find a
        // valid transition to it or jump directly there.
        final int targetIndex = mState.indexOfKeyframe(stateSet);
        boolean changed = targetIndex != getCurrentIndex()
                && (selectTransition(targetIndex) || selectDrawable(targetIndex));

        // We need to propagate the state change to the current drawable, but
        // we can't call StateListDrawable.onStateChange() without changing the
        // current drawable.
        final Drawable current = getCurrent();
        if (current != null) {
            changed |= current.setState(stateSet);
        }

        return changed;
    }

    private boolean selectTransition(int toIndex) {
        final int fromIndex;
        final Transition currentTransition = mTransition;
        if (currentTransition != null) {
            if (toIndex == mTransitionToIndex) {
                // Already animating to that keyframe.
                return true;
            } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) {
                // Reverse the current animation.
                currentTransition.reverse();
                mTransitionToIndex = mTransitionFromIndex;
                mTransitionFromIndex = toIndex;
                return true;
            }

            // Start the next transition from the end of the current one.
            fromIndex = mTransitionToIndex;

            // Changing animation, end the current animation.
            currentTransition.stop();
        } else {
            fromIndex = getCurrentIndex();
        }

        // Reset state.
        mTransition = null;
        mTransitionFromIndex = -1;
        mTransitionToIndex = -1;

        final AnimatedStateListState state = mState;
        final int fromId = state.getKeyframeIdAt(fromIndex);
        final int toId = state.getKeyframeIdAt(toIndex);
        if (toId == 0 || fromId == 0) {
            // Missing a keyframe ID.
            return false;
        }

        final int transitionIndex = state.indexOfTransition(fromId, toId);
        if (transitionIndex < 0) {
            // Couldn't select a transition.
            return false;
        }

        boolean hasReversibleFlag = state.transitionHasReversibleFlag(fromId, toId);

        // This may fail if we're already on the transition, but that's okay!
        selectDrawable(transitionIndex);

        final Transition transition;
        final Drawable d = getCurrent();
        if (d instanceof AnimationDrawable) {
            final boolean reversed = state.isTransitionReversed(fromId, toId);

            transition = new AnimationDrawableTransition((AnimationDrawable) d,
                    reversed, hasReversibleFlag);
        } else if (d instanceof AnimatedVectorDrawable) {
            final boolean reversed = state.isTransitionReversed(fromId, toId);

            transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d,
                    reversed, hasReversibleFlag);
        } else if (d instanceof Animatable) {
            transition = new AnimatableTransition((Animatable) d);
        } else {
            // We don't know how to animate this transition.
            return false;
        }

        transition.start();

        mTransition = transition;
        mTransitionFromIndex = fromIndex;
        mTransitionToIndex = toIndex;
        return true;
    }

    private static abstract class Transition {
        public abstract void start();
        public abstract void stop();

        public void reverse() {
            // Not supported by default.
        }

        public boolean canReverse() {
            return false;
        }
    }

    private static class AnimatableTransition  extends Transition {
        private final Animatable mA;

        public AnimatableTransition(Animatable a) {
            mA = a;
        }

        @Override
        public void start() {
            mA.start();
        }

        @Override
        public void stop() {
            mA.stop();
        }
    }


    private static class AnimationDrawableTransition  extends Transition {
        private final ObjectAnimator mAnim;

        // Even AnimationDrawable is always reversible technically, but
        // we should obey the XML's android:reversible flag.
        private final boolean mHasReversibleFlag;

        public AnimationDrawableTransition(AnimationDrawable ad,
                boolean reversed, boolean hasReversibleFlag) {
            final int frameCount = ad.getNumberOfFrames();
            final int fromFrame = reversed ? frameCount - 1 : 0;
            final int toFrame = reversed ? 0 : frameCount - 1;
            final FrameInterpolator interp = new FrameInterpolator(ad, reversed);
            final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame);
            anim.setAutoCancel(true);
            anim.setDuration(interp.getTotalDuration());
            anim.setInterpolator(interp);
            mHasReversibleFlag = hasReversibleFlag;
            mAnim = anim;
        }

        @Override
        public boolean canReverse() {
            return mHasReversibleFlag;
        }

        @Override
        public void start() {
            mAnim.start();
        }

        @Override
        public void reverse() {
            mAnim.reverse();
        }

        @Override
        public void stop() {
            mAnim.cancel();
        }
    }

    private static class AnimatedVectorDrawableTransition  extends Transition {
        private final AnimatedVectorDrawable mAvd;

        // mReversed is indicating the current transition's direction.
        private final boolean mReversed;

        // mHasReversibleFlag is indicating whether the whole transition has
        // reversible flag set to true.
        // If mHasReversibleFlag is false, then mReversed is always false.
        private final boolean mHasReversibleFlag;

        public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd,
                boolean reversed, boolean hasReversibleFlag) {
            mAvd = avd;
            mReversed = reversed;
            mHasReversibleFlag = hasReversibleFlag;
        }

        @Override
        public boolean canReverse() {
            // When the transition's XML says it is not reversible, then we obey
            // it, even if the AVD itself is reversible.
            // This will help the single direction transition.
            return mAvd.canReverse() && mHasReversibleFlag;
        }

        @Override
        public void start() {
            if (mReversed) {
                reverse();
            } else {
                mAvd.start();
            }
        }

        @Override
        public void reverse() {
            if (canReverse()) {
                mAvd.reverse();
            } else {
                Log.w(LOGTAG, "Can't reverse, either the reversible is set to false,"
                        + " or the AnimatedVectorDrawable can't reverse");
            }
        }

        @Override
        public void stop() {
            mAvd.stop();
        }
    }


    @Override
    public void jumpToCurrentState() {
        super.jumpToCurrentState();

        if (mTransition != null) {
            mTransition.stop();
            mTransition = null;

            selectDrawable(mTransitionToIndex);
            mTransitionToIndex = -1;
            mTransitionFromIndex = -1;
        }
    }

    @Override
    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs, @Nullable Theme theme)
            throws XmlPullParserException, IOException {
        final TypedArray a = obtainAttributes(
                r, theme, attrs, R.styleable.AnimatedStateListDrawable);
        super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible);
        updateStateFromTypedArray(a);
        a.recycle();

        inflateChildElements(r, parser, attrs, theme);

        init();
    }

    @Override
    public void applyTheme(@Nullable Theme theme) {
        super.applyTheme(theme);

        final AnimatedStateListState state = mState;
        if (state == null || state.mAnimThemeAttrs == null) {
            return;
        }

        final TypedArray a = theme.resolveAttributes(
                state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable);
        updateStateFromTypedArray(a);
        a.recycle();

        init();
    }

    private void updateStateFromTypedArray(TypedArray a) {
        final AnimatedStateListState state = mState;

        // Account for any configuration changes.
        state.mChangingConfigurations |= a.getChangingConfigurations();

        // Extract the theme attributes, if any.
        state.mAnimThemeAttrs = a.extractThemeAttrs();

        state.setVariablePadding(a.getBoolean(
                R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding));
        state.setConstantSize(a.getBoolean(
                R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize));
        state.setEnterFadeDuration(a.getInt(
                R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration));
        state.setExitFadeDuration(a.getInt(
                R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration));

        setDither(a.getBoolean(
                R.styleable.AnimatedStateListDrawable_dither, state.mDither));
        setAutoMirrored(a.getBoolean(
                R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored));
    }

    private void init() {
        onStateChange(getState());
    }

    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
            Theme theme) throws XmlPullParserException, IOException {
        int type;

        final int innerDepth = parser.getDepth() + 1;
        int depth;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth
                || type != XmlPullParser.END_TAG)) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            if (depth > innerDepth) {
                continue;
            }

            if (parser.getName().equals(ELEMENT_ITEM)) {
                parseItem(r, parser, attrs, theme);
            } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
                parseTransition(r, parser, attrs, theme);
            }
        }
    }

    private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs, @Nullable Theme theme)
            throws XmlPullParserException, IOException {
        // This allows state list drawable item elements to be themed at
        // inflation time but does NOT make them work for Zygote preload.
        final TypedArray a = obtainAttributes(r, theme, attrs,
                R.styleable.AnimatedStateListDrawableTransition);
        final int fromId = a.getResourceId(
                R.styleable.AnimatedStateListDrawableTransition_fromId, 0);
        final int toId = a.getResourceId(
                R.styleable.AnimatedStateListDrawableTransition_toId, 0);
        final boolean reversible = a.getBoolean(
                R.styleable.AnimatedStateListDrawableTransition_reversible, false);
        Drawable dr = a.getDrawable(
                R.styleable.AnimatedStateListDrawableTransition_drawable);
        a.recycle();

        // Loading child elements modifies the state of the AttributeSet's
        // underlying parser, so it needs to happen after obtaining
        // attributes and extracting states.
        if (dr == null) {
            int type;
            while ((type = parser.next()) == XmlPullParser.TEXT) {
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException(
                        parser.getPositionDescription()
                                + ": <transition> tag requires a 'drawable' attribute or "
                                + "child tag defining a drawable");
            }
            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
        }

        return mState.addTransition(fromId, toId, dr, reversible);
    }

    private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs, @Nullable Theme theme)
            throws XmlPullParserException, IOException {
        // This allows state list drawable item elements to be themed at
        // inflation time but does NOT make them work for Zygote preload.
        final TypedArray a = obtainAttributes(r, theme, attrs,
                R.styleable.AnimatedStateListDrawableItem);
        final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0);
        Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable);
        a.recycle();

        final int[] states = extractStateSet(attrs);

        // Loading child elements modifies the state of the AttributeSet's
        // underlying parser, so it needs to happen after obtaining
        // attributes and extracting states.
        if (dr == null) {
            int type;
            while ((type = parser.next()) == XmlPullParser.TEXT) {
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException(
                        parser.getPositionDescription()
                                + ": <item> tag requires a 'drawable' attribute or "
                                + "child tag defining a drawable");
            }
            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
        }

        return mState.addStateSet(states, dr, keyframeId);
    }

    @Override
    public Drawable mutate() {
        if (!mMutated && super.mutate() == this) {
            mState.mutate();
            mMutated = true;
        }

        return this;
    }

    @Override
    AnimatedStateListState cloneConstantState() {
        return new AnimatedStateListState(mState, this, null);
    }

    /**
     * @hide
     */
    public void clearMutated() {
        super.clearMutated();
        mMutated = false;
    }

    static class AnimatedStateListState extends StateListState {
        // REVERSED_BIT is indicating the current transition's direction.
        private static final long REVERSED_BIT = 0x100000000l;

        // REVERSIBLE_FLAG_BIT is indicating whether the whole transition has
        // reversible flag set to true.
        private static final long REVERSIBLE_FLAG_BIT = 0x200000000l;

        int[] mAnimThemeAttrs;

        LongSparseLongArray mTransitions;
        SparseIntArray mStateIds;

        AnimatedStateListState(@Nullable AnimatedStateListState orig,
                @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) {
            super(orig, owner, res);

            if (orig != null) {
                // Perform a shallow copy and rely on mutate() to deep-copy.
                mAnimThemeAttrs = orig.mAnimThemeAttrs;
                mTransitions = orig.mTransitions;
                mStateIds = orig.mStateIds;
            } else {
                mTransitions = new LongSparseLongArray();
                mStateIds = new SparseIntArray();
            }
        }

        void mutate() {
            mTransitions = mTransitions.clone();
            mStateIds = mStateIds.clone();
        }

        int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) {
            final int pos = super.addChild(anim);
            final long keyFromTo = generateTransitionKey(fromId, toId);
            long reversibleBit = 0;
            if (reversible) {
                reversibleBit = REVERSIBLE_FLAG_BIT;
            }
            mTransitions.append(keyFromTo, pos | reversibleBit);

            if (reversible) {
                final long keyToFrom = generateTransitionKey(toId, fromId);
                mTransitions.append(keyToFrom, pos | REVERSED_BIT | reversibleBit);
            }

            return pos;
        }

        int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
            final int index = super.addStateSet(stateSet, drawable);
            mStateIds.put(index, id);
            return index;
        }

        int indexOfKeyframe(@NonNull int[] stateSet) {
            final int index = super.indexOfStateSet(stateSet);
            if (index >= 0) {
                return index;
            }

            return super.indexOfStateSet(StateSet.WILD_CARD);
        }

        int getKeyframeIdAt(int index) {
            return index < 0 ? 0 : mStateIds.get(index, 0);
        }

        int indexOfTransition(int fromId, int toId) {
            final long keyFromTo = generateTransitionKey(fromId, toId);
            return (int) mTransitions.get(keyFromTo, -1);
        }

        boolean isTransitionReversed(int fromId, int toId) {
            final long keyFromTo = generateTransitionKey(fromId, toId);
            return (mTransitions.get(keyFromTo, -1) & REVERSED_BIT) != 0;
        }

        boolean transitionHasReversibleFlag(int fromId, int toId) {
            final long keyFromTo = generateTransitionKey(fromId, toId);
            return (mTransitions.get(keyFromTo, -1) & REVERSIBLE_FLAG_BIT) != 0;
        }

        @Override
        public boolean canApplyTheme() {
            return mAnimThemeAttrs != null || super.canApplyTheme();
        }

        @Override
        public Drawable newDrawable() {
            return new AnimatedStateListDrawable(this, null);
        }

        @Override
        public Drawable newDrawable(Resources res) {
            return new AnimatedStateListDrawable(this, res);
        }

        private static long generateTransitionKey(int fromId, int toId) {
            return (long) fromId << 32 | toId;
        }
    }

    @Override
    protected void setConstantState(@NonNull DrawableContainerState state) {
        super.setConstantState(state);

        if (state instanceof AnimatedStateListState) {
            mState = (AnimatedStateListState) state;
        }
    }

    private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) {
        super(null);

        // Every animated state list drawable has its own constant state.
        final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
        setConstantState(newState);
        onStateChange(getState());
        jumpToCurrentState();
    }

    /**
     * Interpolates between frames with respect to their individual durations.
     */
    private static class FrameInterpolator implements TimeInterpolator {
        private int[] mFrameTimes;
        private int mFrames;
        private int mTotalDuration;

        public FrameInterpolator(AnimationDrawable d, boolean reversed) {
            updateFrames(d, reversed);
        }

        public int updateFrames(AnimationDrawable d, boolean reversed) {
            final int N = d.getNumberOfFrames();
            mFrames = N;

            if (mFrameTimes == null || mFrameTimes.length < N) {
                mFrameTimes = new int[N];
            }

            final int[] frameTimes = mFrameTimes;
            int totalDuration = 0;
            for (int i = 0; i < N; i++) {
                final int duration = d.getDuration(reversed ? N - i - 1 : i);
                frameTimes[i] = duration;
                totalDuration += duration;
            }

            mTotalDuration = totalDuration;
            return totalDuration;
        }

        public int getTotalDuration() {
            return mTotalDuration;
        }

        @Override
        public float getInterpolation(float input) {
            final int elapsed = (int) (input * mTotalDuration + 0.5f);
            final int N = mFrames;
            final int[] frameTimes = mFrameTimes;

            // Find the current frame and remaining time within that frame.
            int remaining = elapsed;
            int i = 0;
            while (i < N && remaining >= frameTimes[i]) {
                remaining -= frameTimes[i];
                i++;
            }

            // Remaining time is relative of total duration.
            final float frameElapsed;
            if (i < N) {
                frameElapsed = remaining / (float) mTotalDuration;
            } else {
                frameElapsed = 0;
            }

            return i / (float) N + frameElapsed;
        }
    }
}