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