AnimatedStateListDrawable.java revision f1f5f6fcaa768c5b88e9a56f18cbd6ecf72755a8
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        // If we're not already at the target index, either attempt to find a
143        // valid transition to it or jump directly there.
144        final int targetIndex = mState.indexOfKeyframe(stateSet);
145        boolean changed = targetIndex != getCurrentIndex()
146                && (selectTransition(targetIndex) || selectDrawable(targetIndex));
147
148        // We need to propagate the state change to the current drawable, but
149        // we can't call StateListDrawable.onStateChange() without changing the
150        // current drawable.
151        final Drawable current = getCurrent();
152        if (current != null) {
153            changed |= current.setState(stateSet);
154        }
155
156        return changed;
157    }
158
159    private boolean selectTransition(int toIndex) {
160        final int fromIndex;
161        final Transition currentTransition = mTransition;
162        if (currentTransition != null) {
163            if (toIndex == mTransitionToIndex) {
164                // Already animating to that keyframe.
165                return true;
166            } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) {
167                // Reverse the current animation.
168                currentTransition.reverse();
169                mTransitionToIndex = mTransitionFromIndex;
170                mTransitionFromIndex = toIndex;
171                return true;
172            }
173
174            // Start the next transition from the end of the current one.
175            fromIndex = mTransitionToIndex;
176
177            // Changing animation, end the current animation.
178            currentTransition.stop();
179        } else {
180            fromIndex = getCurrentIndex();
181        }
182
183        // Reset state.
184        mTransition = null;
185        mTransitionFromIndex = -1;
186        mTransitionToIndex = -1;
187
188        final AnimatedStateListState state = mState;
189        final int fromId = state.getKeyframeIdAt(fromIndex);
190        final int toId = state.getKeyframeIdAt(toIndex);
191        if (toId == 0 || fromId == 0) {
192            // Missing a keyframe ID.
193            return false;
194        }
195
196        final int transitionIndex = state.indexOfTransition(fromId, toId);
197        if (transitionIndex < 0) {
198            // Couldn't select a transition.
199            return false;
200        }
201
202        boolean hasReversibleFlag = state.transitionHasReversibleFlag(fromId, toId);
203
204        // This may fail if we're already on the transition, but that's okay!
205        selectDrawable(transitionIndex);
206
207        final Transition transition;
208        final Drawable d = getCurrent();
209        if (d instanceof AnimationDrawable) {
210            final boolean reversed = state.isTransitionReversed(fromId, toId);
211
212            transition = new AnimationDrawableTransition((AnimationDrawable) d,
213                    reversed, hasReversibleFlag);
214        } else if (d instanceof AnimatedVectorDrawable) {
215            final boolean reversed = state.isTransitionReversed(fromId, toId);
216
217            transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d,
218                    reversed, hasReversibleFlag);
219        } else if (d instanceof Animatable) {
220            transition = new AnimatableTransition((Animatable) d);
221        } else {
222            // We don't know how to animate this transition.
223            return false;
224        }
225
226        transition.start();
227
228        mTransition = transition;
229        mTransitionFromIndex = fromIndex;
230        mTransitionToIndex = toIndex;
231        return true;
232    }
233
234    private static abstract class Transition {
235        public abstract void start();
236        public abstract void stop();
237
238        public void reverse() {
239            // Not supported by default.
240        }
241
242        public boolean canReverse() {
243            return false;
244        }
245    }
246
247    private static class AnimatableTransition  extends Transition {
248        private final Animatable mA;
249
250        public AnimatableTransition(Animatable a) {
251            mA = a;
252        }
253
254        @Override
255        public void start() {
256            mA.start();
257        }
258
259        @Override
260        public void stop() {
261            mA.stop();
262        }
263    }
264
265
266    private static class AnimationDrawableTransition  extends Transition {
267        private final ObjectAnimator mAnim;
268
269        // Even AnimationDrawable is always reversible technically, but
270        // we should obey the XML's android:reversible flag.
271        private final boolean mHasReversibleFlag;
272
273        public AnimationDrawableTransition(AnimationDrawable ad,
274                boolean reversed, boolean hasReversibleFlag) {
275            final int frameCount = ad.getNumberOfFrames();
276            final int fromFrame = reversed ? frameCount - 1 : 0;
277            final int toFrame = reversed ? 0 : frameCount - 1;
278            final FrameInterpolator interp = new FrameInterpolator(ad, reversed);
279            final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame);
280            anim.setAutoCancel(true);
281            anim.setDuration(interp.getTotalDuration());
282            anim.setInterpolator(interp);
283            mHasReversibleFlag = hasReversibleFlag;
284            mAnim = anim;
285        }
286
287        @Override
288        public boolean canReverse() {
289            return mHasReversibleFlag;
290        }
291
292        @Override
293        public void start() {
294            mAnim.start();
295        }
296
297        @Override
298        public void reverse() {
299            mAnim.reverse();
300        }
301
302        @Override
303        public void stop() {
304            mAnim.cancel();
305        }
306    }
307
308    private static class AnimatedVectorDrawableTransition  extends Transition {
309        private final AnimatedVectorDrawable mAvd;
310
311        // mReversed is indicating the current transition's direction.
312        private final boolean mReversed;
313
314        // mHasReversibleFlag is indicating whether the whole transition has
315        // reversible flag set to true.
316        // If mHasReversibleFlag is false, then mReversed is always false.
317        private final boolean mHasReversibleFlag;
318
319        public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd,
320                boolean reversed, boolean hasReversibleFlag) {
321            mAvd = avd;
322            mReversed = reversed;
323            mHasReversibleFlag = hasReversibleFlag;
324        }
325
326        @Override
327        public boolean canReverse() {
328            // When the transition's XML says it is not reversible, then we obey
329            // it, even if the AVD itself is reversible.
330            // This will help the single direction transition.
331            return mAvd.canReverse() && mHasReversibleFlag;
332        }
333
334        @Override
335        public void start() {
336            if (mReversed) {
337                reverse();
338            } else {
339                mAvd.start();
340            }
341        }
342
343        @Override
344        public void reverse() {
345            if (canReverse()) {
346                mAvd.reverse();
347            } else {
348                Log.w(LOGTAG, "Can't reverse, either the reversible is set to false,"
349                        + " or the AnimatedVectorDrawable can't reverse");
350            }
351        }
352
353        @Override
354        public void stop() {
355            mAvd.stop();
356        }
357    }
358
359
360    @Override
361    public void jumpToCurrentState() {
362        super.jumpToCurrentState();
363
364        if (mTransition != null) {
365            mTransition.stop();
366            mTransition = null;
367
368            selectDrawable(mTransitionToIndex);
369            mTransitionToIndex = -1;
370            mTransitionFromIndex = -1;
371        }
372    }
373
374    @Override
375    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
376            @NonNull AttributeSet attrs, @Nullable Theme theme)
377            throws XmlPullParserException, IOException {
378        final TypedArray a = obtainAttributes(
379                r, theme, attrs, R.styleable.AnimatedStateListDrawable);
380        super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible);
381        updateStateFromTypedArray(a);
382        updateDensity(r);
383        a.recycle();
384
385        inflateChildElements(r, parser, attrs, theme);
386
387        init();
388    }
389
390    @Override
391    public void applyTheme(@Nullable Theme theme) {
392        super.applyTheme(theme);
393
394        final AnimatedStateListState state = mState;
395        if (state == null || state.mAnimThemeAttrs == null) {
396            return;
397        }
398
399        final TypedArray a = theme.resolveAttributes(
400                state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable);
401        updateStateFromTypedArray(a);
402        a.recycle();
403
404        init();
405    }
406
407    private void updateStateFromTypedArray(TypedArray a) {
408        final AnimatedStateListState state = mState;
409
410        // Account for any configuration changes.
411        state.mChangingConfigurations |= a.getChangingConfigurations();
412
413        // Extract the theme attributes, if any.
414        state.mAnimThemeAttrs = a.extractThemeAttrs();
415
416        state.setVariablePadding(a.getBoolean(
417                R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding));
418        state.setConstantSize(a.getBoolean(
419                R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize));
420        state.setEnterFadeDuration(a.getInt(
421                R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration));
422        state.setExitFadeDuration(a.getInt(
423                R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration));
424
425        setDither(a.getBoolean(
426                R.styleable.AnimatedStateListDrawable_dither, state.mDither));
427        setAutoMirrored(a.getBoolean(
428                R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored));
429    }
430
431    private void init() {
432        onStateChange(getState());
433    }
434
435    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
436            Theme theme) throws XmlPullParserException, IOException {
437        int type;
438
439        final int innerDepth = parser.getDepth() + 1;
440        int depth;
441        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
442                && ((depth = parser.getDepth()) >= innerDepth
443                || type != XmlPullParser.END_TAG)) {
444            if (type != XmlPullParser.START_TAG) {
445                continue;
446            }
447
448            if (depth > innerDepth) {
449                continue;
450            }
451
452            if (parser.getName().equals(ELEMENT_ITEM)) {
453                parseItem(r, parser, attrs, theme);
454            } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
455                parseTransition(r, parser, attrs, theme);
456            }
457        }
458    }
459
460    private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser,
461            @NonNull AttributeSet attrs, @Nullable Theme theme)
462            throws XmlPullParserException, IOException {
463        // This allows state list drawable item elements to be themed at
464        // inflation time but does NOT make them work for Zygote preload.
465        final TypedArray a = obtainAttributes(r, theme, attrs,
466                R.styleable.AnimatedStateListDrawableTransition);
467        final int fromId = a.getResourceId(
468                R.styleable.AnimatedStateListDrawableTransition_fromId, 0);
469        final int toId = a.getResourceId(
470                R.styleable.AnimatedStateListDrawableTransition_toId, 0);
471        final boolean reversible = a.getBoolean(
472                R.styleable.AnimatedStateListDrawableTransition_reversible, false);
473        Drawable dr = a.getDrawable(
474                R.styleable.AnimatedStateListDrawableTransition_drawable);
475        a.recycle();
476
477        // Loading child elements modifies the state of the AttributeSet's
478        // underlying parser, so it needs to happen after obtaining
479        // attributes and extracting states.
480        if (dr == null) {
481            int type;
482            while ((type = parser.next()) == XmlPullParser.TEXT) {
483            }
484            if (type != XmlPullParser.START_TAG) {
485                throw new XmlPullParserException(
486                        parser.getPositionDescription()
487                                + ": <transition> tag requires a 'drawable' attribute or "
488                                + "child tag defining a drawable");
489            }
490            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
491        }
492
493        return mState.addTransition(fromId, toId, dr, reversible);
494    }
495
496    private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser,
497            @NonNull AttributeSet attrs, @Nullable Theme theme)
498            throws XmlPullParserException, IOException {
499        // This allows state list drawable item elements to be themed at
500        // inflation time but does NOT make them work for Zygote preload.
501        final TypedArray a = obtainAttributes(r, theme, attrs,
502                R.styleable.AnimatedStateListDrawableItem);
503        final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0);
504        Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable);
505        a.recycle();
506
507        final int[] states = extractStateSet(attrs);
508
509        // Loading child elements modifies the state of the AttributeSet's
510        // underlying parser, so it needs to happen after obtaining
511        // attributes and extracting states.
512        if (dr == null) {
513            int type;
514            while ((type = parser.next()) == XmlPullParser.TEXT) {
515            }
516            if (type != XmlPullParser.START_TAG) {
517                throw new XmlPullParserException(
518                        parser.getPositionDescription()
519                                + ": <item> tag requires a 'drawable' attribute or "
520                                + "child tag defining a drawable");
521            }
522            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
523        }
524
525        return mState.addStateSet(states, dr, keyframeId);
526    }
527
528    @Override
529    public Drawable mutate() {
530        if (!mMutated && super.mutate() == this) {
531            mState.mutate();
532            mMutated = true;
533        }
534
535        return this;
536    }
537
538    @Override
539    AnimatedStateListState cloneConstantState() {
540        return new AnimatedStateListState(mState, this, null);
541    }
542
543    /**
544     * @hide
545     */
546    public void clearMutated() {
547        super.clearMutated();
548        mMutated = false;
549    }
550
551    static class AnimatedStateListState extends StateListState {
552        // REVERSED_BIT is indicating the current transition's direction.
553        private static final long REVERSED_BIT = 0x100000000l;
554
555        // REVERSIBLE_FLAG_BIT is indicating whether the whole transition has
556        // reversible flag set to true.
557        private static final long REVERSIBLE_FLAG_BIT = 0x200000000l;
558
559        int[] mAnimThemeAttrs;
560
561        LongSparseLongArray mTransitions;
562        SparseIntArray mStateIds;
563
564        AnimatedStateListState(@Nullable AnimatedStateListState orig,
565                @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) {
566            super(orig, owner, res);
567
568            if (orig != null) {
569                // Perform a shallow copy and rely on mutate() to deep-copy.
570                mAnimThemeAttrs = orig.mAnimThemeAttrs;
571                mTransitions = orig.mTransitions;
572                mStateIds = orig.mStateIds;
573            } else {
574                mTransitions = new LongSparseLongArray();
575                mStateIds = new SparseIntArray();
576            }
577        }
578
579        void mutate() {
580            mTransitions = mTransitions.clone();
581            mStateIds = mStateIds.clone();
582        }
583
584        int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) {
585            final int pos = super.addChild(anim);
586            final long keyFromTo = generateTransitionKey(fromId, toId);
587            long reversibleBit = 0;
588            if (reversible) {
589                reversibleBit = REVERSIBLE_FLAG_BIT;
590            }
591            mTransitions.append(keyFromTo, pos | reversibleBit);
592
593            if (reversible) {
594                final long keyToFrom = generateTransitionKey(toId, fromId);
595                mTransitions.append(keyToFrom, pos | REVERSED_BIT | reversibleBit);
596            }
597
598            return pos;
599        }
600
601        int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
602            final int index = super.addStateSet(stateSet, drawable);
603            mStateIds.put(index, id);
604            return index;
605        }
606
607        int indexOfKeyframe(@NonNull int[] stateSet) {
608            final int index = super.indexOfStateSet(stateSet);
609            if (index >= 0) {
610                return index;
611            }
612
613            return super.indexOfStateSet(StateSet.WILD_CARD);
614        }
615
616        int getKeyframeIdAt(int index) {
617            return index < 0 ? 0 : mStateIds.get(index, 0);
618        }
619
620        int indexOfTransition(int fromId, int toId) {
621            final long keyFromTo = generateTransitionKey(fromId, toId);
622            return (int) mTransitions.get(keyFromTo, -1);
623        }
624
625        boolean isTransitionReversed(int fromId, int toId) {
626            final long keyFromTo = generateTransitionKey(fromId, toId);
627            return (mTransitions.get(keyFromTo, -1) & REVERSED_BIT) != 0;
628        }
629
630        boolean transitionHasReversibleFlag(int fromId, int toId) {
631            final long keyFromTo = generateTransitionKey(fromId, toId);
632            return (mTransitions.get(keyFromTo, -1) & REVERSIBLE_FLAG_BIT) != 0;
633        }
634
635        @Override
636        public boolean canApplyTheme() {
637            return mAnimThemeAttrs != null || super.canApplyTheme();
638        }
639
640        @Override
641        public Drawable newDrawable() {
642            return new AnimatedStateListDrawable(this, null);
643        }
644
645        @Override
646        public Drawable newDrawable(Resources res) {
647            return new AnimatedStateListDrawable(this, res);
648        }
649
650        private static long generateTransitionKey(int fromId, int toId) {
651            return (long) fromId << 32 | toId;
652        }
653    }
654
655    @Override
656    protected void setConstantState(@NonNull DrawableContainerState state) {
657        super.setConstantState(state);
658
659        if (state instanceof AnimatedStateListState) {
660            mState = (AnimatedStateListState) state;
661        }
662    }
663
664    private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) {
665        super(null);
666
667        // Every animated state list drawable has its own constant state.
668        final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
669        setConstantState(newState);
670        onStateChange(getState());
671        jumpToCurrentState();
672    }
673
674    /**
675     * Interpolates between frames with respect to their individual durations.
676     */
677    private static class FrameInterpolator implements TimeInterpolator {
678        private int[] mFrameTimes;
679        private int mFrames;
680        private int mTotalDuration;
681
682        public FrameInterpolator(AnimationDrawable d, boolean reversed) {
683            updateFrames(d, reversed);
684        }
685
686        public int updateFrames(AnimationDrawable d, boolean reversed) {
687            final int N = d.getNumberOfFrames();
688            mFrames = N;
689
690            if (mFrameTimes == null || mFrameTimes.length < N) {
691                mFrameTimes = new int[N];
692            }
693
694            final int[] frameTimes = mFrameTimes;
695            int totalDuration = 0;
696            for (int i = 0; i < N; i++) {
697                final int duration = d.getDuration(reversed ? N - i - 1 : i);
698                frameTimes[i] = duration;
699                totalDuration += duration;
700            }
701
702            mTotalDuration = totalDuration;
703            return totalDuration;
704        }
705
706        public int getTotalDuration() {
707            return mTotalDuration;
708        }
709
710        @Override
711        public float getInterpolation(float input) {
712            final int elapsed = (int) (input * mTotalDuration + 0.5f);
713            final int N = mFrames;
714            final int[] frameTimes = mFrameTimes;
715
716            // Find the current frame and remaining time within that frame.
717            int remaining = elapsed;
718            int i = 0;
719            while (i < N && remaining >= frameTimes[i]) {
720                remaining -= frameTimes[i];
721                i++;
722            }
723
724            // Remaining time is relative of total duration.
725            final float frameElapsed;
726            if (i < N) {
727                frameElapsed = remaining / (float) mTotalDuration;
728            } else {
729                frameElapsed = 0;
730            }
731
732            return i / (float) N + frameElapsed;
733        }
734    }
735}
736