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