AnimatedStateListDrawable.java revision 8dcd533786df8d824f1e040230ee9e7e5b083998
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        a.recycle();
383
384        inflateChildElements(r, parser, attrs, theme);
385
386        init();
387    }
388
389    @Override
390    public void applyTheme(@Nullable Theme theme) {
391        super.applyTheme(theme);
392
393        final AnimatedStateListState state = mState;
394        if (state == null || state.mAnimThemeAttrs == null) {
395            return;
396        }
397
398        final TypedArray a = theme.resolveAttributes(
399                state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable);
400        updateStateFromTypedArray(a);
401        a.recycle();
402
403        init();
404    }
405
406    private void updateStateFromTypedArray(TypedArray a) {
407        final AnimatedStateListState state = mState;
408
409        // Account for any configuration changes.
410        state.mChangingConfigurations |= a.getChangingConfigurations();
411
412        // Extract the theme attributes, if any.
413        state.mAnimThemeAttrs = a.extractThemeAttrs();
414
415        state.setVariablePadding(a.getBoolean(
416                R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding));
417        state.setConstantSize(a.getBoolean(
418                R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize));
419        state.setEnterFadeDuration(a.getInt(
420                R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration));
421        state.setExitFadeDuration(a.getInt(
422                R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration));
423
424        setDither(a.getBoolean(
425                R.styleable.AnimatedStateListDrawable_dither, state.mDither));
426        setAutoMirrored(a.getBoolean(
427                R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored));
428    }
429
430    private void init() {
431        onStateChange(getState());
432    }
433
434    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
435            Theme theme) throws XmlPullParserException, IOException {
436        int type;
437
438        final int innerDepth = parser.getDepth() + 1;
439        int depth;
440        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
441                && ((depth = parser.getDepth()) >= innerDepth
442                || type != XmlPullParser.END_TAG)) {
443            if (type != XmlPullParser.START_TAG) {
444                continue;
445            }
446
447            if (depth > innerDepth) {
448                continue;
449            }
450
451            if (parser.getName().equals(ELEMENT_ITEM)) {
452                parseItem(r, parser, attrs, theme);
453            } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
454                parseTransition(r, parser, attrs, theme);
455            }
456        }
457    }
458
459    private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser,
460            @NonNull AttributeSet attrs, @Nullable Theme theme)
461            throws XmlPullParserException, IOException {
462        // This allows state list drawable item elements to be themed at
463        // inflation time but does NOT make them work for Zygote preload.
464        final TypedArray a = obtainAttributes(r, theme, attrs,
465                R.styleable.AnimatedStateListDrawableTransition);
466        final int fromId = a.getResourceId(
467                R.styleable.AnimatedStateListDrawableTransition_fromId, 0);
468        final int toId = a.getResourceId(
469                R.styleable.AnimatedStateListDrawableTransition_toId, 0);
470        final boolean reversible = a.getBoolean(
471                R.styleable.AnimatedStateListDrawableTransition_reversible, false);
472        Drawable dr = a.getDrawable(
473                R.styleable.AnimatedStateListDrawableTransition_drawable);
474        a.recycle();
475
476        // Loading child elements modifies the state of the AttributeSet's
477        // underlying parser, so it needs to happen after obtaining
478        // attributes and extracting states.
479        if (dr == null) {
480            int type;
481            while ((type = parser.next()) == XmlPullParser.TEXT) {
482            }
483            if (type != XmlPullParser.START_TAG) {
484                throw new XmlPullParserException(
485                        parser.getPositionDescription()
486                                + ": <transition> tag requires a 'drawable' attribute or "
487                                + "child tag defining a drawable");
488            }
489            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
490        }
491
492        return mState.addTransition(fromId, toId, dr, reversible);
493    }
494
495    private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser,
496            @NonNull AttributeSet attrs, @Nullable Theme theme)
497            throws XmlPullParserException, IOException {
498        // This allows state list drawable item elements to be themed at
499        // inflation time but does NOT make them work for Zygote preload.
500        final TypedArray a = obtainAttributes(r, theme, attrs,
501                R.styleable.AnimatedStateListDrawableItem);
502        final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0);
503        Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable);
504        a.recycle();
505
506        final int[] states = extractStateSet(attrs);
507
508        // Loading child elements modifies the state of the AttributeSet's
509        // underlying parser, so it needs to happen after obtaining
510        // attributes and extracting states.
511        if (dr == null) {
512            int type;
513            while ((type = parser.next()) == XmlPullParser.TEXT) {
514            }
515            if (type != XmlPullParser.START_TAG) {
516                throw new XmlPullParserException(
517                        parser.getPositionDescription()
518                                + ": <item> tag requires a 'drawable' attribute or "
519                                + "child tag defining a drawable");
520            }
521            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
522        }
523
524        return mState.addStateSet(states, dr, keyframeId);
525    }
526
527    @Override
528    public Drawable mutate() {
529        if (!mMutated && super.mutate() == this) {
530            mState.mutate();
531            mMutated = true;
532        }
533
534        return this;
535    }
536
537    @Override
538    AnimatedStateListState cloneConstantState() {
539        return new AnimatedStateListState(mState, this, null);
540    }
541
542    /**
543     * @hide
544     */
545    public void clearMutated() {
546        super.clearMutated();
547        mMutated = false;
548    }
549
550    static class AnimatedStateListState extends StateListState {
551        // REVERSED_BIT is indicating the current transition's direction.
552        private static final long REVERSED_BIT = 0x100000000l;
553
554        // REVERSIBLE_FLAG_BIT is indicating whether the whole transition has
555        // reversible flag set to true.
556        private static final long REVERSIBLE_FLAG_BIT = 0x200000000l;
557
558        int[] mAnimThemeAttrs;
559
560        LongSparseLongArray mTransitions;
561        SparseIntArray mStateIds;
562
563        AnimatedStateListState(@Nullable AnimatedStateListState orig,
564                @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) {
565            super(orig, owner, res);
566
567            if (orig != null) {
568                // Perform a shallow copy and rely on mutate() to deep-copy.
569                mAnimThemeAttrs = orig.mAnimThemeAttrs;
570                mTransitions = orig.mTransitions;
571                mStateIds = orig.mStateIds;
572            } else {
573                mTransitions = new LongSparseLongArray();
574                mStateIds = new SparseIntArray();
575            }
576        }
577
578        private void mutate() {
579            mTransitions = mTransitions.clone();
580            mStateIds = mStateIds.clone();
581        }
582
583        int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) {
584            final int pos = super.addChild(anim);
585            final long keyFromTo = generateTransitionKey(fromId, toId);
586            long reversibleBit = 0;
587            if (reversible) {
588                reversibleBit = REVERSIBLE_FLAG_BIT;
589            }
590            mTransitions.append(keyFromTo, pos | reversibleBit);
591
592            if (reversible) {
593                final long keyToFrom = generateTransitionKey(toId, fromId);
594                mTransitions.append(keyToFrom, pos | REVERSED_BIT | reversibleBit);
595            }
596
597            return addChild(anim);
598        }
599
600        int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
601            final int index = super.addStateSet(stateSet, drawable);
602            mStateIds.put(index, id);
603            return index;
604        }
605
606        int indexOfKeyframe(@NonNull int[] stateSet) {
607            final int index = super.indexOfStateSet(stateSet);
608            if (index >= 0) {
609                return index;
610            }
611
612            return super.indexOfStateSet(StateSet.WILD_CARD);
613        }
614
615        int getKeyframeIdAt(int index) {
616            return index < 0 ? 0 : mStateIds.get(index, 0);
617        }
618
619        int indexOfTransition(int fromId, int toId) {
620            final long keyFromTo = generateTransitionKey(fromId, toId);
621            return (int) mTransitions.get(keyFromTo, -1);
622        }
623
624        boolean isTransitionReversed(int fromId, int toId) {
625            final long keyFromTo = generateTransitionKey(fromId, toId);
626            return (mTransitions.get(keyFromTo, -1) & REVERSED_BIT) != 0;
627        }
628
629        boolean transitionHasReversibleFlag(int fromId, int toId) {
630            final long keyFromTo = generateTransitionKey(fromId, toId);
631            return (mTransitions.get(keyFromTo, -1) & REVERSIBLE_FLAG_BIT) != 0;
632        }
633
634        @Override
635        public boolean canApplyTheme() {
636            return mAnimThemeAttrs != null || super.canApplyTheme();
637        }
638
639        @Override
640        public Drawable newDrawable() {
641            return new AnimatedStateListDrawable(this, null);
642        }
643
644        @Override
645        public Drawable newDrawable(Resources res) {
646            return new AnimatedStateListDrawable(this, res);
647        }
648
649        private static long generateTransitionKey(int fromId, int toId) {
650            return (long) fromId << 32 | toId;
651        }
652    }
653
654    protected void setConstantState(@NonNull DrawableContainerState state) {
655        super.setConstantState(state);
656
657        if (state instanceof AnimatedStateListState) {
658            mState = (AnimatedStateListState) state;
659        }
660    }
661
662    private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) {
663        super(null);
664
665        // Every animated state list drawable has its own constant state.
666        final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
667        setConstantState(newState);
668        onStateChange(getState());
669        jumpToCurrentState();
670    }
671
672    /**
673     * Interpolates between frames with respect to their individual durations.
674     */
675    private static class FrameInterpolator implements TimeInterpolator {
676        private int[] mFrameTimes;
677        private int mFrames;
678        private int mTotalDuration;
679
680        public FrameInterpolator(AnimationDrawable d, boolean reversed) {
681            updateFrames(d, reversed);
682        }
683
684        public int updateFrames(AnimationDrawable d, boolean reversed) {
685            final int N = d.getNumberOfFrames();
686            mFrames = N;
687
688            if (mFrameTimes == null || mFrameTimes.length < N) {
689                mFrameTimes = new int[N];
690            }
691
692            final int[] frameTimes = mFrameTimes;
693            int totalDuration = 0;
694            for (int i = 0; i < N; i++) {
695                final int duration = d.getDuration(reversed ? N - i - 1 : i);
696                frameTimes[i] = duration;
697                totalDuration += duration;
698            }
699
700            mTotalDuration = totalDuration;
701            return totalDuration;
702        }
703
704        public int getTotalDuration() {
705            return mTotalDuration;
706        }
707
708        @Override
709        public float getInterpolation(float input) {
710            final int elapsed = (int) (input * mTotalDuration + 0.5f);
711            final int N = mFrames;
712            final int[] frameTimes = mFrameTimes;
713
714            // Find the current frame and remaining time within that frame.
715            int remaining = elapsed;
716            int i = 0;
717            while (i < N && remaining >= frameTimes[i]) {
718                remaining -= frameTimes[i];
719                i++;
720            }
721
722            // Remaining time is relative of total duration.
723            final float frameElapsed;
724            if (i < N) {
725                frameElapsed = remaining / (float) mTotalDuration;
726            } else {
727                frameElapsed = 0;
728            }
729
730            return i / (float) N + frameElapsed;
731        }
732    }
733}
734