AnimatedStateListDrawable.java revision d646fa2933ddcbfd4ed9c7686bf5e8253b4ba980
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.ObjectAnimator;
20import android.animation.TimeInterpolator;
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.content.res.Resources;
24import android.content.res.Resources.Theme;
25import android.content.res.TypedArray;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.util.LongSparseLongArray;
29import android.util.SparseIntArray;
30import android.util.StateSet;
31
32import com.android.internal.R;
33
34import org.xmlpull.v1.XmlPullParser;
35import org.xmlpull.v1.XmlPullParserException;
36
37import 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 */
63public 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                mTransition.stop();
95            }
96        }
97
98        return changed;
99    }
100
101    /**
102     * Add a new drawable to the set of keyframes.
103     *
104     * @param stateSet An array of resource IDs to associate with the keyframe
105     * @param drawable The drawable to show when in the specified state, may not be null
106     * @param id The unique identifier for the keyframe
107     */
108    public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
109        if (drawable == null) {
110            throw new IllegalArgumentException("Drawable must not be null");
111        }
112
113        mState.addStateSet(stateSet, drawable, id);
114        onStateChange(getState());
115    }
116
117    /**
118     * Adds a new transition between keyframes.
119     *
120     * @param fromId Unique identifier of the starting keyframe
121     * @param toId Unique identifier of the ending keyframe
122     * @param transition An animatable drawable to use as a transition, may not be null
123     * @param reversible Whether the transition can be reversed
124     */
125    public void addTransition(int fromId, int toId, @NonNull Drawable transition,
126            boolean reversible) {
127        if (transition == null) {
128            throw new IllegalArgumentException("Transition drawable must not be null");
129        }
130
131        mState.addTransition(fromId, toId, transition, reversible);
132    }
133
134    @Override
135    public boolean isStateful() {
136        return true;
137    }
138
139    @Override
140    protected boolean onStateChange(int[] stateSet) {
141        final int keyframeIndex = mState.indexOfKeyframe(stateSet);
142        if (keyframeIndex == getCurrentIndex()) {
143            // No transition needed.
144            return false;
145        }
146
147        // Attempt to find a valid transition to the keyframe.
148        if (selectTransition(keyframeIndex)) {
149            return true;
150        }
151
152        // No valid transition, attempt to jump directly to the keyframe.
153        if (selectDrawable(keyframeIndex)) {
154            return true;
155        }
156
157        return super.onStateChange(stateSet);
158    }
159
160    private boolean selectTransition(int toIndex) {
161        final int fromIndex;
162        final Transition currentTransition = mTransition;
163        if (currentTransition != null) {
164            if (toIndex == mTransitionToIndex) {
165                // Already animating to that keyframe.
166                return true;
167            } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) {
168                // Reverse the current animation.
169                currentTransition.reverse();
170                mTransitionToIndex = mTransitionFromIndex;
171                mTransitionFromIndex = toIndex;
172                return true;
173            }
174
175            // Start the next transition from the end of the current one.
176            fromIndex = mTransitionToIndex;
177
178            // Changing animation, end the current animation.
179            currentTransition.stop();
180        } else {
181            fromIndex = getCurrentIndex();
182        }
183
184        // Reset state.
185        mTransition = null;
186        mTransitionFromIndex = -1;
187        mTransitionToIndex = -1;
188
189        final AnimatedStateListState state = mState;
190        final int fromId = state.getKeyframeIdAt(fromIndex);
191        final int toId = state.getKeyframeIdAt(toIndex);
192        if (toId == 0 || fromId == 0) {
193            // Missing a keyframe ID.
194            return false;
195        }
196
197        final int transitionIndex = state.indexOfTransition(fromId, toId);
198        if (transitionIndex < 0) {
199            // Couldn't select a transition.
200            return false;
201        }
202
203        // This may fail if we're already on the transition, but that's okay!
204        selectDrawable(transitionIndex);
205
206        final Transition transition;
207        final Drawable d = getCurrent();
208        if (d instanceof AnimationDrawable) {
209            final boolean reversed = state.isTransitionReversed(fromId, toId);
210            transition = new AnimationDrawableTransition((AnimationDrawable) d, reversed);
211        } else if (d instanceof AnimatedVectorDrawable) {
212            final boolean reversed = state.isTransitionReversed(fromId, toId);
213            transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d, reversed);
214        } else if (d instanceof Animatable) {
215            transition = new AnimatableTransition((Animatable) d);
216        } else {
217            // We don't know how to animate this transition.
218            return false;
219        }
220
221        transition.start();
222
223        mTransition = transition;
224        mTransitionFromIndex = fromIndex;
225        mTransitionToIndex = toIndex;
226        return true;
227    }
228
229    private static abstract class Transition {
230        public abstract void start();
231        public abstract void stop();
232
233        public void reverse() {
234            // Not supported by default.
235        }
236
237        public boolean canReverse() {
238            return false;
239        }
240    }
241
242    private static class AnimatableTransition  extends Transition {
243        private final Animatable mA;
244
245        public AnimatableTransition(Animatable a) {
246            mA = a;
247        }
248
249        @Override
250        public void start() {
251            mA.start();
252        }
253
254        @Override
255        public void stop() {
256            mA.stop();
257        }
258    }
259
260
261    private static class AnimationDrawableTransition  extends Transition {
262        private final ObjectAnimator mAnim;
263
264        public AnimationDrawableTransition(AnimationDrawable ad, boolean reversed) {
265            final int frameCount = ad.getNumberOfFrames();
266            final int fromFrame = reversed ? frameCount - 1 : 0;
267            final int toFrame = reversed ? 0 : frameCount - 1;
268            final FrameInterpolator interp = new FrameInterpolator(ad, reversed);
269            final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame);
270            anim.setAutoCancel(true);
271            anim.setDuration(interp.getTotalDuration());
272            anim.setInterpolator(interp);
273
274            mAnim = anim;
275        }
276
277        @Override
278        public boolean canReverse() {
279            return true;
280        }
281
282        @Override
283        public void start() {
284            mAnim.start();
285        }
286
287        @Override
288        public void reverse() {
289            mAnim.reverse();
290        }
291
292        @Override
293        public void stop() {
294            mAnim.cancel();
295        }
296    }
297
298    private static class AnimatedVectorDrawableTransition  extends Transition {
299        private final AnimatedVectorDrawable mAvd;
300        private final boolean mReversed;
301
302        public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd, boolean reversed) {
303            mAvd = avd;
304            mReversed = reversed;
305        }
306
307        @Override
308        public boolean canReverse() {
309            return mAvd.canReverse();
310        }
311
312        @Override
313        public void start() {
314            if (mReversed) {
315                reverse();
316            } else {
317                mAvd.start();
318            }
319        }
320
321        @Override
322        public void reverse() {
323            if (canReverse()) {
324                mAvd.reverse();
325            } else {
326                Log.w(LOGTAG, "Reverse() is called on a drawable can't reverse");
327            }
328        }
329
330        @Override
331        public void stop() {
332            mAvd.stop();
333        }
334    }
335
336
337    @Override
338    public void jumpToCurrentState() {
339        super.jumpToCurrentState();
340
341        if (mTransition != null) {
342            mTransition.stop();
343            mTransition = null;
344
345            selectDrawable(mTransitionToIndex);
346            mTransitionToIndex = -1;
347            mTransitionFromIndex = -1;
348        }
349    }
350
351    @Override
352    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
353            @NonNull AttributeSet attrs, @Nullable Theme theme)
354            throws XmlPullParserException, IOException {
355        final TypedArray a = r.obtainAttributes(attrs, R.styleable.AnimatedStateListDrawable);
356
357        super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible);
358
359        final StateListState stateListState = getStateListState();
360        stateListState.setVariablePadding(a.getBoolean(
361                R.styleable.AnimatedStateListDrawable_variablePadding, false));
362        stateListState.setConstantSize(a.getBoolean(
363                R.styleable.AnimatedStateListDrawable_constantSize, false));
364        stateListState.setEnterFadeDuration(a.getInt(
365                R.styleable.AnimatedStateListDrawable_enterFadeDuration, 0));
366        stateListState.setExitFadeDuration(a.getInt(
367                R.styleable.AnimatedStateListDrawable_exitFadeDuration, 0));
368
369        setDither(a.getBoolean(R.styleable.AnimatedStateListDrawable_dither, true));
370        setAutoMirrored(a.getBoolean(R.styleable.AnimatedStateListDrawable_autoMirrored, false));
371
372        a.recycle();
373
374        int type;
375
376        final int innerDepth = parser.getDepth() + 1;
377        int depth;
378        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
379                && ((depth = parser.getDepth()) >= innerDepth
380                || type != XmlPullParser.END_TAG)) {
381            if (type != XmlPullParser.START_TAG) {
382                continue;
383            }
384
385            if (depth > innerDepth) {
386                continue;
387            }
388
389            if (parser.getName().equals(ELEMENT_ITEM)) {
390                parseItem(r, parser, attrs, theme);
391            } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
392                parseTransition(r, parser, attrs, theme);
393            }
394        }
395
396        onStateChange(getState());
397    }
398
399    private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser,
400            @NonNull AttributeSet attrs, @Nullable Theme theme)
401            throws XmlPullParserException, IOException {
402        int drawableRes = 0;
403        int fromId = 0;
404        int toId = 0;
405        boolean reversible = false;
406
407        final int numAttrs = attrs.getAttributeCount();
408        for (int i = 0; i < numAttrs; i++) {
409            final int stateResId = attrs.getAttributeNameResource(i);
410            switch (stateResId) {
411                case 0:
412                    break;
413                case R.attr.fromId:
414                    fromId = attrs.getAttributeResourceValue(i, 0);
415                    break;
416                case R.attr.toId:
417                    toId = attrs.getAttributeResourceValue(i, 0);
418                    break;
419                case R.attr.drawable:
420                    drawableRes = attrs.getAttributeResourceValue(i, 0);
421                    break;
422                case R.attr.reversible:
423                    reversible = attrs.getAttributeBooleanValue(i, false);
424                    break;
425            }
426        }
427
428        final Drawable dr;
429        if (drawableRes != 0) {
430            dr = r.getDrawable(drawableRes);
431        } else {
432            int type;
433            while ((type = parser.next()) == XmlPullParser.TEXT) {
434            }
435            if (type != XmlPullParser.START_TAG) {
436                throw new XmlPullParserException(
437                        parser.getPositionDescription()
438                                + ": <item> tag requires a 'drawable' attribute or "
439                                + "child tag defining a drawable");
440            }
441            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
442        }
443
444        return mState.addTransition(fromId, toId, dr, reversible);
445    }
446
447    private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser,
448            @NonNull AttributeSet attrs, @Nullable Theme theme)
449            throws XmlPullParserException, IOException {
450        int drawableRes = 0;
451        int keyframeId = 0;
452
453        int j = 0;
454        final int numAttrs = attrs.getAttributeCount();
455        int[] states = new int[numAttrs];
456        for (int i = 0; i < numAttrs; i++) {
457            final int stateResId = attrs.getAttributeNameResource(i);
458            switch (stateResId) {
459                case 0:
460                    break;
461                case R.attr.id:
462                    keyframeId = attrs.getAttributeResourceValue(i, 0);
463                    break;
464                case R.attr.drawable:
465                    drawableRes = attrs.getAttributeResourceValue(i, 0);
466                    break;
467                default:
468                    final boolean hasState = attrs.getAttributeBooleanValue(i, false);
469                    states[j++] = hasState ? stateResId : -stateResId;
470            }
471        }
472        states = StateSet.trimStateSet(states, j);
473
474        final Drawable dr;
475        if (drawableRes != 0) {
476            dr = r.getDrawable(drawableRes);
477        } else {
478            int type;
479            while ((type = parser.next()) == XmlPullParser.TEXT) {
480            }
481            if (type != XmlPullParser.START_TAG) {
482                throw new XmlPullParserException(
483                        parser.getPositionDescription()
484                                + ": <item> tag requires a 'drawable' attribute or "
485                                + "child tag defining a drawable");
486            }
487            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
488        }
489
490        return mState.addStateSet(states, dr, keyframeId);
491    }
492
493    @Override
494    public Drawable mutate() {
495        if (!mMutated) {
496            final AnimatedStateListState newState = new AnimatedStateListState(mState, this, null);
497            setConstantState(newState);
498            mMutated = true;
499        }
500
501        return this;
502    }
503
504    static class AnimatedStateListState extends StateListState {
505        private static final int REVERSE_SHIFT = 32;
506        private static final int REVERSE_MASK = 0x1;
507
508        final LongSparseLongArray mTransitions;
509        final SparseIntArray mStateIds;
510
511        AnimatedStateListState(@Nullable AnimatedStateListState orig,
512                @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) {
513            super(orig, owner, res);
514
515            if (orig != null) {
516                mTransitions = orig.mTransitions.clone();
517                mStateIds = orig.mStateIds.clone();
518            } else {
519                mTransitions = new LongSparseLongArray();
520                mStateIds = new SparseIntArray();
521            }
522        }
523
524        int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) {
525            final int pos = super.addChild(anim);
526            final long keyFromTo = generateTransitionKey(fromId, toId);
527            mTransitions.append(keyFromTo, pos);
528
529            if (reversible) {
530                final long keyToFrom = generateTransitionKey(toId, fromId);
531                mTransitions.append(keyToFrom, pos | (1L << REVERSE_SHIFT));
532            }
533
534            return addChild(anim);
535        }
536
537        int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
538            final int index = super.addStateSet(stateSet, drawable);
539            mStateIds.put(index, id);
540            return index;
541        }
542
543        int indexOfKeyframe(@NonNull int[] stateSet) {
544            final int index = super.indexOfStateSet(stateSet);
545            if (index >= 0) {
546                return index;
547            }
548
549            return super.indexOfStateSet(StateSet.WILD_CARD);
550        }
551
552        int getKeyframeIdAt(int index) {
553            return index < 0 ? 0 : mStateIds.get(index, 0);
554        }
555
556        int indexOfTransition(int fromId, int toId) {
557            final long keyFromTo = generateTransitionKey(fromId, toId);
558            return (int) mTransitions.get(keyFromTo, -1);
559        }
560
561        boolean isTransitionReversed(int fromId, int toId) {
562            final long keyFromTo = generateTransitionKey(fromId, toId);
563            return (mTransitions.get(keyFromTo, -1) >> REVERSE_SHIFT & REVERSE_MASK) == 1;
564        }
565
566        @Override
567        public Drawable newDrawable() {
568            return new AnimatedStateListDrawable(this, null);
569        }
570
571        @Override
572        public Drawable newDrawable(Resources res) {
573            return new AnimatedStateListDrawable(this, res);
574        }
575
576        private static long generateTransitionKey(int fromId, int toId) {
577            return (long) fromId << 32 | toId;
578        }
579    }
580
581    void setConstantState(@NonNull AnimatedStateListState state) {
582        super.setConstantState(state);
583
584        mState = state;
585    }
586
587    private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) {
588        super(null);
589
590        final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
591        setConstantState(newState);
592        onStateChange(getState());
593        jumpToCurrentState();
594    }
595
596    /**
597     * Interpolates between frames with respect to their individual durations.
598     */
599    private static class FrameInterpolator implements TimeInterpolator {
600        private int[] mFrameTimes;
601        private int mFrames;
602        private int mTotalDuration;
603
604        public FrameInterpolator(AnimationDrawable d, boolean reversed) {
605            updateFrames(d, reversed);
606        }
607
608        public int updateFrames(AnimationDrawable d, boolean reversed) {
609            final int N = d.getNumberOfFrames();
610            mFrames = N;
611
612            if (mFrameTimes == null || mFrameTimes.length < N) {
613                mFrameTimes = new int[N];
614            }
615
616            final int[] frameTimes = mFrameTimes;
617            int totalDuration = 0;
618            for (int i = 0; i < N; i++) {
619                final int duration = d.getDuration(reversed ? N - i - 1 : i);
620                frameTimes[i] = duration;
621                totalDuration += duration;
622            }
623
624            mTotalDuration = totalDuration;
625            return totalDuration;
626        }
627
628        public int getTotalDuration() {
629            return mTotalDuration;
630        }
631
632        @Override
633        public float getInterpolation(float input) {
634            final int elapsed = (int) (input * mTotalDuration + 0.5f);
635            final int N = mFrames;
636            final int[] frameTimes = mFrameTimes;
637
638            // Find the current frame and remaining time within that frame.
639            int remaining = elapsed;
640            int i = 0;
641            while (i < N && remaining >= frameTimes[i]) {
642                remaining -= frameTimes[i];
643                i++;
644            }
645
646            // Remaining time is relative of total duration.
647            final float frameElapsed;
648            if (i < N) {
649                frameElapsed = remaining / (float) mTotalDuration;
650            } else {
651                frameElapsed = 0;
652            }
653
654            return i / (float) N + frameElapsed;
655        }
656    }
657}
658