Home | History | Annotate | Download | only in drawable
      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 
     82     public AnimatedStateListDrawable() {
     83         this(null, null);
     84     }
     85 
     86     @Override
     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      */
    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      */
    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
    136     public boolean isStateful() {
    137         return true;
    138     }
    139 
    140     @Override
    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 
    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 {
    235         public abstract void start();
    236         public abstract void stop();
    237 
    238         public void reverse() {
    239             // Not supported by default.
    240         }
    241 
    242         public boolean canReverse() {
    243             return false;
    244         }
    245     }
    246 
    247     private static class AnimatableTransition  extends Transition {
    248         private final Animatable mA;
    249 
    250         public AnimatableTransition(Animatable a) {
    251             mA = a;
    252         }
    253 
    254         @Override
    255         public void start() {
    256             mA.start();
    257         }
    258 
    259         @Override
    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 
    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
    288         public boolean canReverse() {
    289             return mHasReversibleFlag;
    290         }
    291 
    292         @Override
    293         public void start() {
    294             mAnim.start();
    295         }
    296 
    297         @Override
    298         public void reverse() {
    299             mAnim.reverse();
    300         }
    301 
    302         @Override
    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 
    319         public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd,
    320                 boolean reversed, boolean hasReversibleFlag) {
    321             mAvd = avd;
    322             mReversed = reversed;
    323             mHasReversibleFlag = hasReversibleFlag;
    324         }
    325 
    326         @Override
    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
    335         public void start() {
    336             if (mReversed) {
    337                 reverse();
    338             } else {
    339                 mAvd.start();
    340             }
    341         }
    342 
    343         @Override
    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
    354         public void stop() {
    355             mAvd.stop();
    356         }
    357     }
    358 
    359 
    360     @Override
    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
    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
    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 
    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 
    431     private void init() {
    432         onStateChange(getState());
    433     }
    434 
    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 
    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 
    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
    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
    539     AnimatedStateListState cloneConstantState() {
    540         return new AnimatedStateListState(mState, this, null);
    541     }
    542 
    543     /**
    544      * @hide
    545      */
    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 
    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 
    579         void mutate() {
    580             mTransitions = mTransitions.clone();
    581             mStateIds = mStateIds.clone();
    582         }
    583 
    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 
    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 
    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 
    616         int getKeyframeIdAt(int index) {
    617             return index < 0 ? 0 : mStateIds.get(index, 0);
    618         }
    619 
    620         int indexOfTransition(int fromId, int toId) {
    621             final long keyFromTo = generateTransitionKey(fromId, toId);
    622             return (int) mTransitions.get(keyFromTo, -1);
    623         }
    624 
    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 
    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
    636         public boolean canApplyTheme() {
    637             return mAnimThemeAttrs != null || super.canApplyTheme();
    638         }
    639 
    640         @Override
    641         public Drawable newDrawable() {
    642             return new AnimatedStateListDrawable(this, null);
    643         }
    644 
    645         @Override
    646         public Drawable newDrawable(Resources res) {
    647             return new AnimatedStateListDrawable(this, res);
    648         }
    649 
    650         private static long generateTransitionKey(int fromId, int toId) {
    651             return (long) fromId << 32 | toId;
    652         }
    653     }
    654 
    655     @Override
    656     protected void setConstantState(@NonNull DrawableContainerState state) {
    657         super.setConstantState(state);
    658 
    659         if (state instanceof AnimatedStateListState) {
    660             mState = (AnimatedStateListState) state;
    661         }
    662     }
    663 
    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 
    682         public FrameInterpolator(AnimationDrawable d, boolean reversed) {
    683             updateFrames(d, reversed);
    684         }
    685 
    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 
    706         public int getTotalDuration() {
    707             return mTotalDuration;
    708         }
    709 
    710         @Override
    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