AnimatedStateListDrawable.java revision 97fb0aa5090858705b66bfc4c05e7530c5d3d6b1
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
17package android.graphics.drawable;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.content.res.Resources;
24import android.content.res.Resources.Theme;
25import android.content.res.TypedArray;
26import android.util.AttributeSet;
27import android.util.LongSparseLongArray;
28import android.util.SparseIntArray;
29import android.util.StateSet;
30
31import com.android.internal.R;
32
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35
36import java.io.IOException;
37
38/**
39 * Drawable containing a set of Drawable keyframes where the currently displayed
40 * keyframe is chosen based on the current state set. Animations between
41 * keyframes may optionally be defined using transition elements.
42 * <p>
43 * This drawable can be defined in an XML file with the <code>
44 * &lt;animated-selector></code> element. Each keyframe Drawable is defined in a
45 * nested <code>&lt;item></code> element. Transitions are defined in a nested
46 * <code>&lt;transition></code> element.
47 *
48 * @attr ref android.R.styleable#DrawableStates_state_focused
49 * @attr ref android.R.styleable#DrawableStates_state_window_focused
50 * @attr ref android.R.styleable#DrawableStates_state_enabled
51 * @attr ref android.R.styleable#DrawableStates_state_checkable
52 * @attr ref android.R.styleable#DrawableStates_state_checked
53 * @attr ref android.R.styleable#DrawableStates_state_selected
54 * @attr ref android.R.styleable#DrawableStates_state_activated
55 * @attr ref android.R.styleable#DrawableStates_state_active
56 * @attr ref android.R.styleable#DrawableStates_state_single
57 * @attr ref android.R.styleable#DrawableStates_state_first
58 * @attr ref android.R.styleable#DrawableStates_state_middle
59 * @attr ref android.R.styleable#DrawableStates_state_last
60 * @attr ref android.R.styleable#DrawableStates_state_pressed
61 */
62public class AnimatedStateListDrawable extends StateListDrawable {
63    private static final String ELEMENT_TRANSITION = "transition";
64    private static final String ELEMENT_ITEM = "item";
65
66    private AnimatedStateListState mState;
67
68    /** The currently running animation, if any. */
69    private ObjectAnimator mAnim;
70
71    /** Index to be set after the animation ends. */
72    private int mAnimToIndex = -1;
73
74    /** Index away from which we are animating. */
75    private int mAnimFromIndex = -1;
76
77    private boolean mMutated;
78
79    public AnimatedStateListDrawable() {
80        this(null, null);
81    }
82
83    @Override
84    public boolean setVisible(boolean visible, boolean restart) {
85        final boolean changed = super.setVisible(visible, restart);
86        if (mAnim != null) {
87            if (visible) {
88                if (changed || restart) {
89                    // TODO: Should this support restart?
90                    mAnim.end();
91                }
92            } else {
93                mAnim.end();
94            }
95        }
96        return changed;
97    }
98
99    /**
100     * Add a new drawable to the set of keyframes.
101     *
102     * @param stateSet An array of resource IDs to associate with the keyframe
103     * @param drawable The drawable to show when in the specified state
104     * @param id The unique identifier for the keyframe
105     */
106    public void addState(int[] stateSet, Drawable drawable, int id) {
107        if (drawable != null) {
108            mState.addStateSet(stateSet, drawable, id);
109            onStateChange(getState());
110        }
111    }
112
113    /**
114     * Adds a new transition between keyframes.
115     *
116     * @param fromId Unique identifier of the starting keyframe
117     * @param toId Unique identifier of the ending keyframe
118     * @param anim An AnimationDrawable to use as a transition
119     * @param reversible Whether the transition can be reversed
120     */
121    public void addTransition(int fromId, int toId, AnimationDrawable anim, boolean reversible) {
122        mState.addTransition(fromId, toId, anim, reversible);
123    }
124
125    @Override
126    public boolean isStateful() {
127        return true;
128    }
129
130    @Override
131    protected boolean onStateChange(int[] stateSet) {
132        final int keyframeIndex = mState.indexOfKeyframe(stateSet);
133        if (keyframeIndex == getCurrentIndex()) {
134            return false;
135        }
136
137        if (selectTransition(keyframeIndex)) {
138            return true;
139        }
140
141        if (selectDrawable(keyframeIndex)) {
142            return true;
143        }
144
145        return super.onStateChange(stateSet);
146    }
147
148    private boolean selectTransition(int toIndex) {
149        if (mAnim != null) {
150            if (toIndex == mAnimToIndex) {
151                // Already animating to that keyframe.
152                return true;
153            } else if (toIndex == mAnimFromIndex) {
154                // Reverse the current animation.
155                mAnim.reverse();
156                mAnimFromIndex = mAnimToIndex;
157                mAnimToIndex = toIndex;
158                return true;
159            }
160
161            // Changing animation, end the current animation.
162            mAnim.end();
163        }
164
165        final AnimatedStateListState state = mState;
166        final int fromIndex = getCurrentIndex();
167        final int fromId = state.getKeyframeIdAt(fromIndex);
168        final int toId = state.getKeyframeIdAt(toIndex);
169
170        if (toId == 0 || fromId == 0) {
171            // Missing a keyframe ID.
172            return false;
173        }
174
175        final int transitionIndex = state.indexOfTransition(fromId, toId);
176        if (transitionIndex < 0 || !selectDrawable(transitionIndex)) {
177            // Couldn't select a transition.
178            return false;
179        }
180
181        final Drawable d = getCurrent();
182        if (!(d instanceof AnimationDrawable)) {
183            // Transition isn't an animation.
184            return false;
185        }
186
187        final AnimationDrawable ad = (AnimationDrawable) d;
188        final boolean reversed = mState.isTransitionReversed(fromId, toId);
189        final int frameCount = ad.getNumberOfFrames();
190        final int fromFrame = reversed ? frameCount - 1 : 0;
191        final int toFrame = reversed ? 0 : frameCount - 1;
192
193        final FrameInterpolator interp = new FrameInterpolator(ad, reversed);
194        final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame);
195        anim.setAutoCancel(true);
196        anim.setDuration(interp.getTotalDuration());
197        anim.addListener(mAnimListener);
198        anim.setInterpolator(interp);
199        anim.start();
200
201        mAnim = anim;
202        mAnimFromIndex = fromIndex;
203        mAnimToIndex = toIndex;
204        return true;
205    }
206
207    @Override
208    public void jumpToCurrentState() {
209        super.jumpToCurrentState();
210
211        if (mAnim != null) {
212            mAnim.end();
213        }
214    }
215
216    @Override
217    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
218            throws XmlPullParserException, IOException {
219        final TypedArray a = r.obtainAttributes(attrs, R.styleable.AnimatedStateListDrawable);
220
221        super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible);
222
223        final StateListState stateListState = getStateListState();
224        stateListState.setVariablePadding(a.getBoolean(
225                R.styleable.AnimatedStateListDrawable_variablePadding, false));
226        stateListState.setConstantSize(a.getBoolean(
227                R.styleable.AnimatedStateListDrawable_constantSize, false));
228        stateListState.setEnterFadeDuration(a.getInt(
229                R.styleable.AnimatedStateListDrawable_enterFadeDuration, 0));
230        stateListState.setExitFadeDuration(a.getInt(
231                R.styleable.AnimatedStateListDrawable_exitFadeDuration, 0));
232
233        setDither(a.getBoolean(R.styleable.AnimatedStateListDrawable_dither, true));
234        setAutoMirrored(a.getBoolean(R.styleable.AnimatedStateListDrawable_autoMirrored, false));
235
236        a.recycle();
237
238        int type;
239
240        final int innerDepth = parser.getDepth() + 1;
241        int depth;
242        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
243                && ((depth = parser.getDepth()) >= innerDepth
244                || type != XmlPullParser.END_TAG)) {
245            if (type != XmlPullParser.START_TAG) {
246                continue;
247            }
248
249            if (depth > innerDepth) {
250                continue;
251            }
252
253            if (parser.getName().equals(ELEMENT_ITEM)) {
254                parseItem(r, parser, attrs, theme);
255            } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
256                parseTransition(r, parser, attrs, theme);
257            }
258        }
259
260        onStateChange(getState());
261    }
262
263    private int parseTransition(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
264            throws XmlPullParserException, IOException {
265        int drawableRes = 0;
266        int fromId = 0;
267        int toId = 0;
268        boolean reversible = false;
269
270        final int numAttrs = attrs.getAttributeCount();
271        for (int i = 0; i < numAttrs; i++) {
272            final int stateResId = attrs.getAttributeNameResource(i);
273            switch (stateResId) {
274                case 0:
275                    break;
276                case R.attr.fromId:
277                    fromId = attrs.getAttributeResourceValue(i, 0);
278                    break;
279                case R.attr.toId:
280                    toId = attrs.getAttributeResourceValue(i, 0);
281                    break;
282                case R.attr.drawable:
283                    drawableRes = attrs.getAttributeResourceValue(i, 0);
284                    break;
285                case R.attr.reversible:
286                    reversible = attrs.getAttributeBooleanValue(i, false);
287                    break;
288            }
289        }
290
291        final Drawable dr;
292        if (drawableRes != 0) {
293            dr = r.getDrawable(drawableRes);
294        } else {
295            int type;
296            while ((type = parser.next()) == XmlPullParser.TEXT) {
297            }
298            if (type != XmlPullParser.START_TAG) {
299                throw new XmlPullParserException(
300                        parser.getPositionDescription()
301                                + ": <item> tag requires a 'drawable' attribute or "
302                                + "child tag defining a drawable");
303            }
304            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
305        }
306
307        final AnimationDrawable anim;
308        if (dr instanceof AnimationDrawable) {
309            anim = (AnimationDrawable) dr;
310        } else {
311            throw new XmlPullParserException(parser.getPositionDescription()
312                    + ": <transition> tag requires a 'drawable' attribute or "
313                    + "child tag defining a drawable of type <animation>");
314        }
315
316        return mState.addTransition(fromId, toId, anim, reversible);
317    }
318
319    private int parseItem(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
320            throws XmlPullParserException, IOException {
321        int drawableRes = 0;
322        int keyframeId = 0;
323
324        int j = 0;
325        final int numAttrs = attrs.getAttributeCount();
326        int[] states = new int[numAttrs];
327        for (int i = 0; i < numAttrs; i++) {
328            final int stateResId = attrs.getAttributeNameResource(i);
329            switch (stateResId) {
330                case 0:
331                    break;
332                case R.attr.id:
333                    keyframeId = attrs.getAttributeResourceValue(i, 0);
334                    break;
335                case R.attr.drawable:
336                    drawableRes = attrs.getAttributeResourceValue(i, 0);
337                    break;
338                default:
339                    final boolean hasState = attrs.getAttributeBooleanValue(i, false);
340                    states[j++] = hasState ? stateResId : -stateResId;
341            }
342        }
343        states = StateSet.trimStateSet(states, j);
344
345        final Drawable dr;
346        if (drawableRes != 0) {
347            dr = r.getDrawable(drawableRes);
348        } else {
349            int type;
350            while ((type = parser.next()) == XmlPullParser.TEXT) {
351            }
352            if (type != XmlPullParser.START_TAG) {
353                throw new XmlPullParserException(
354                        parser.getPositionDescription()
355                                + ": <item> tag requires a 'drawable' attribute or "
356                                + "child tag defining a drawable");
357            }
358            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
359        }
360
361        return mState.addStateSet(states, dr, keyframeId);
362    }
363
364    @Override
365    public Drawable mutate() {
366        if (!mMutated) {
367            final AnimatedStateListState newState = new AnimatedStateListState(mState, this, null);
368            setConstantState(newState);
369            mMutated = true;
370        }
371
372        return this;
373    }
374
375    private final AnimatorListenerAdapter mAnimListener = new AnimatorListenerAdapter() {
376        @Override
377        public void onAnimationEnd(Animator anim) {
378            selectDrawable(mAnimToIndex);
379
380            mAnimToIndex = -1;
381            mAnimFromIndex = -1;
382            mAnim = null;
383        }
384    };
385
386    static class AnimatedStateListState extends StateListState {
387        private static final int REVERSE_SHIFT = 32;
388        private static final int REVERSE_MASK = 0x1;
389
390        final LongSparseLongArray mTransitions;
391        final SparseIntArray mStateIds;
392
393        AnimatedStateListState(AnimatedStateListState orig, AnimatedStateListDrawable owner,
394                Resources res) {
395            super(orig, owner, res);
396
397            if (orig != null) {
398                mTransitions = orig.mTransitions.clone();
399                mStateIds = orig.mStateIds.clone();
400            } else {
401                mTransitions = new LongSparseLongArray();
402                mStateIds = new SparseIntArray();
403            }
404        }
405
406        int addTransition(int fromId, int toId, AnimationDrawable anim, boolean reversible) {
407            final int pos = super.addChild(anim);
408            final long keyFromTo = generateTransitionKey(fromId, toId);
409            mTransitions.append(keyFromTo, pos);
410
411            if (reversible) {
412                final long keyToFrom = generateTransitionKey(toId, fromId);
413                mTransitions.append(keyToFrom, pos | (1L << REVERSE_SHIFT));
414            }
415
416            return addChild(anim);
417        }
418
419        int addStateSet(int[] stateSet, Drawable drawable, int id) {
420            final int index = super.addStateSet(stateSet, drawable);
421            mStateIds.put(index, id);
422            return index;
423        }
424
425        int indexOfKeyframe(int[] stateSet) {
426            final int index = super.indexOfStateSet(stateSet);
427            if (index >= 0) {
428                return index;
429            }
430
431            return super.indexOfStateSet(StateSet.WILD_CARD);
432        }
433
434        int getKeyframeIdAt(int index) {
435            return index < 0 ? 0 : mStateIds.get(index, 0);
436        }
437
438        int indexOfTransition(int fromId, int toId) {
439            final long keyFromTo = generateTransitionKey(fromId, toId);
440            return (int) mTransitions.get(keyFromTo, -1);
441        }
442
443        boolean isTransitionReversed(int fromId, int toId) {
444            final long keyFromTo = generateTransitionKey(fromId, toId);
445            return (mTransitions.get(keyFromTo, -1) >> REVERSE_SHIFT & REVERSE_MASK) == 1;
446        }
447
448        @Override
449        public Drawable newDrawable() {
450            return new AnimatedStateListDrawable(this, null);
451        }
452
453        @Override
454        public Drawable newDrawable(Resources res) {
455            return new AnimatedStateListDrawable(this, res);
456        }
457
458        private static long generateTransitionKey(int fromId, int toId) {
459            return (long) fromId << 32 | toId;
460        }
461    }
462
463    void setConstantState(AnimatedStateListState state) {
464        super.setConstantState(state);
465
466        mState = state;
467    }
468
469    private AnimatedStateListDrawable(AnimatedStateListState state, Resources res) {
470        super(null);
471
472        final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
473        setConstantState(newState);
474        onStateChange(getState());
475        jumpToCurrentState();
476    }
477
478    /**
479     * Interpolates between frames with respect to their individual durations.
480     */
481    private static class FrameInterpolator implements TimeInterpolator {
482        private int[] mFrameTimes;
483        private int mFrames;
484        private int mTotalDuration;
485
486        public FrameInterpolator(AnimationDrawable d, boolean reversed) {
487            updateFrames(d, reversed);
488        }
489
490        public int updateFrames(AnimationDrawable d, boolean reversed) {
491            final int N = d.getNumberOfFrames();
492            mFrames = N;
493
494            if (mFrameTimes == null || mFrameTimes.length < N) {
495                mFrameTimes = new int[N];
496            }
497
498            final int[] frameTimes = mFrameTimes;
499            int totalDuration = 0;
500            for (int i = 0; i < N; i++) {
501                final int duration = d.getDuration(reversed ? N - i - 1 : i);
502                frameTimes[i] = duration;
503                totalDuration += duration;
504            }
505
506            mTotalDuration = totalDuration;
507            return totalDuration;
508        }
509
510        public int getTotalDuration() {
511            return mTotalDuration;
512        }
513
514        @Override
515        public float getInterpolation(float input) {
516            final int elapsed = (int) (input * mTotalDuration + 0.5f);
517            final int N = mFrames;
518            final int[] frameTimes = mFrameTimes;
519
520            // Find the current frame and remaining time within that frame.
521            int remaining = elapsed;
522            int i = 0;
523            while (i < N && remaining >= frameTimes[i]) {
524                remaining -= frameTimes[i];
525                i++;
526            }
527
528            // Remaining time is relative of total duration.
529            final float frameElapsed;
530            if (i < N) {
531                frameElapsed = remaining / (float) mTotalDuration;
532            } else {
533                frameElapsed = 0;
534            }
535
536            return i / (float) N + frameElapsed;
537        }
538    }
539}
540