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