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