AnimatedStateListDrawable.java revision 7bc6a3f023ca3e1dde91fc97b6036dee3ba538a2
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.graphics.drawable;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.content.res.Resources;
26import android.content.res.Resources.Theme;
27import android.content.res.TypedArray;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.util.LongSparseLongArray;
31import android.util.SparseIntArray;
32import android.util.StateSet;
33
34import com.android.internal.R;
35
36import org.xmlpull.v1.XmlPullParser;
37import org.xmlpull.v1.XmlPullParserException;
38
39import java.io.IOException;
40
41/**
42 * Drawable containing a set of Drawable keyframes where the currently displayed
43 * keyframe is chosen based on the current state set. Animations between
44 * keyframes may optionally be defined using transition elements.
45 * <p>
46 * This drawable can be defined in an XML file with the <code>
47 * &lt;animated-selector></code> element. Each keyframe Drawable is defined in a
48 * nested <code>&lt;item></code> element. Transitions are defined in a nested
49 * <code>&lt;transition></code> element.
50 *
51 * @attr ref android.R.styleable#DrawableStates_state_focused
52 * @attr ref android.R.styleable#DrawableStates_state_window_focused
53 * @attr ref android.R.styleable#DrawableStates_state_enabled
54 * @attr ref android.R.styleable#DrawableStates_state_checkable
55 * @attr ref android.R.styleable#DrawableStates_state_checked
56 * @attr ref android.R.styleable#DrawableStates_state_selected
57 * @attr ref android.R.styleable#DrawableStates_state_activated
58 * @attr ref android.R.styleable#DrawableStates_state_active
59 * @attr ref android.R.styleable#DrawableStates_state_single
60 * @attr ref android.R.styleable#DrawableStates_state_first
61 * @attr ref android.R.styleable#DrawableStates_state_middle
62 * @attr ref android.R.styleable#DrawableStates_state_last
63 * @attr ref android.R.styleable#DrawableStates_state_pressed
64 */
65public class AnimatedStateListDrawable extends StateListDrawable {
66    private static final String LOGTAG = AnimatedStateListDrawable.class.getSimpleName();
67
68    private static final String ELEMENT_TRANSITION = "transition";
69    private static final String ELEMENT_ITEM = "item";
70
71    private AnimatedStateListState mState;
72
73    /** The currently running transition, if any. */
74    private Transition mTransition;
75
76    /** Index to be set after the transition ends. */
77    private int mTransitionToIndex = -1;
78
79    /** Index away from which we are transitioning. */
80    private int mTransitionFromIndex = -1;
81
82    private boolean mMutated;
83
84    public AnimatedStateListDrawable() {
85        this(null, null);
86    }
87
88    @Override
89    public boolean setVisible(boolean visible, boolean restart) {
90        final boolean changed = super.setVisible(visible, restart);
91
92        if (mTransition != null && (changed || restart)) {
93            if (visible) {
94                mTransition.start();
95            } else {
96                mTransition.stop();
97            }
98        }
99
100        return changed;
101    }
102
103    /**
104     * Add a new drawable to the set of keyframes.
105     *
106     * @param stateSet An array of resource IDs to associate with the keyframe
107     * @param drawable The drawable to show when in the specified state, may not be null
108     * @param id The unique identifier for the keyframe
109     */
110    public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
111        if (drawable == null) {
112            throw new IllegalArgumentException("Drawable must not be null");
113        }
114
115        mState.addStateSet(stateSet, drawable, id);
116        onStateChange(getState());
117    }
118
119    /**
120     * Adds a new transition between keyframes.
121     *
122     * @param fromId Unique identifier of the starting keyframe
123     * @param toId Unique identifier of the ending keyframe
124     * @param transition An animatable drawable to use as a transition, may not be null
125     * @param reversible Whether the transition can be reversed
126     */
127    public void addTransition(int fromId, int toId, @NonNull Drawable transition,
128            boolean reversible) {
129        if (transition == null) {
130            throw new IllegalArgumentException("Transition drawable must not be null");
131        }
132
133        mState.addTransition(fromId, toId, transition, reversible);
134    }
135
136    @Override
137    public boolean isStateful() {
138        return true;
139    }
140
141    @Override
142    protected boolean onStateChange(int[] stateSet) {
143        final int keyframeIndex = mState.indexOfKeyframe(stateSet);
144        if (keyframeIndex == getCurrentIndex()) {
145            // No transition needed.
146            return false;
147        }
148
149        // Attempt to find a valid transition to the keyframe.
150        if (selectTransition(keyframeIndex)) {
151            return true;
152        }
153
154        // No valid transition, attempt to jump directly to the keyframe.
155        if (selectDrawable(keyframeIndex)) {
156            return true;
157        }
158
159        return super.onStateChange(stateSet);
160    }
161
162    private boolean selectTransition(int toIndex) {
163        if (toIndex == mTransitionToIndex) {
164            // Already animating to that keyframe.
165            return true;
166        }
167
168        final Transition currentTransition = mTransition;
169        if (currentTransition != null) {
170            if (toIndex == mTransitionToIndex) {
171                return true;
172            } else if (toIndex == mTransitionFromIndex) {
173                // Reverse the current animation.
174                currentTransition.reverse();
175                mTransitionFromIndex = mTransitionToIndex;
176                mTransitionToIndex = toIndex;
177                return true;
178            }
179
180            // Changing animation, end the current animation.
181            currentTransition.stop();
182            mTransition = null;
183        }
184
185        // Reset state.
186        mTransitionFromIndex = -1;
187        mTransitionToIndex = -1;
188
189        final AnimatedStateListState state = mState;
190        final int fromIndex = getCurrentIndex();
191        final int fromId = state.getKeyframeIdAt(fromIndex);
192        final int toId = state.getKeyframeIdAt(toIndex);
193
194        if (toId == 0 || fromId == 0) {
195            // Missing a keyframe ID.
196            return false;
197        }
198
199        final int transitionIndex = state.indexOfTransition(fromId, toId);
200        if (transitionIndex < 0 || !selectDrawable(transitionIndex)) {
201            // Couldn't select a transition.
202            return false;
203        }
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 = r.obtainAttributes(attrs, R.styleable.AnimatedStateListDrawable);
355
356        super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible);
357
358        final StateListState stateListState = getStateListState();
359        stateListState.setVariablePadding(a.getBoolean(
360                R.styleable.AnimatedStateListDrawable_variablePadding, false));
361        stateListState.setConstantSize(a.getBoolean(
362                R.styleable.AnimatedStateListDrawable_constantSize, false));
363        stateListState.setEnterFadeDuration(a.getInt(
364                R.styleable.AnimatedStateListDrawable_enterFadeDuration, 0));
365        stateListState.setExitFadeDuration(a.getInt(
366                R.styleable.AnimatedStateListDrawable_exitFadeDuration, 0));
367
368        setDither(a.getBoolean(R.styleable.AnimatedStateListDrawable_dither, true));
369        setAutoMirrored(a.getBoolean(R.styleable.AnimatedStateListDrawable_autoMirrored, false));
370
371        a.recycle();
372
373        int type;
374
375        final int innerDepth = parser.getDepth() + 1;
376        int depth;
377        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
378                && ((depth = parser.getDepth()) >= innerDepth
379                || type != XmlPullParser.END_TAG)) {
380            if (type != XmlPullParser.START_TAG) {
381                continue;
382            }
383
384            if (depth > innerDepth) {
385                continue;
386            }
387
388            if (parser.getName().equals(ELEMENT_ITEM)) {
389                parseItem(r, parser, attrs, theme);
390            } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
391                parseTransition(r, parser, attrs, theme);
392            }
393        }
394
395        onStateChange(getState());
396    }
397
398    private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser,
399            @NonNull AttributeSet attrs, @Nullable Theme theme)
400            throws XmlPullParserException, IOException {
401        int drawableRes = 0;
402        int fromId = 0;
403        int toId = 0;
404        boolean reversible = false;
405
406        final int numAttrs = attrs.getAttributeCount();
407        for (int i = 0; i < numAttrs; i++) {
408            final int stateResId = attrs.getAttributeNameResource(i);
409            switch (stateResId) {
410                case 0:
411                    break;
412                case R.attr.fromId:
413                    fromId = attrs.getAttributeResourceValue(i, 0);
414                    break;
415                case R.attr.toId:
416                    toId = attrs.getAttributeResourceValue(i, 0);
417                    break;
418                case R.attr.drawable:
419                    drawableRes = attrs.getAttributeResourceValue(i, 0);
420                    break;
421                case R.attr.reversible:
422                    reversible = attrs.getAttributeBooleanValue(i, false);
423                    break;
424            }
425        }
426
427        final Drawable dr;
428        if (drawableRes != 0) {
429            dr = r.getDrawable(drawableRes);
430        } else {
431            int type;
432            while ((type = parser.next()) == XmlPullParser.TEXT) {
433            }
434            if (type != XmlPullParser.START_TAG) {
435                throw new XmlPullParserException(
436                        parser.getPositionDescription()
437                                + ": <item> tag requires a 'drawable' attribute or "
438                                + "child tag defining a drawable");
439            }
440            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
441        }
442
443        return mState.addTransition(fromId, toId, dr, reversible);
444    }
445
446    private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser,
447            @NonNull AttributeSet attrs, @Nullable Theme theme)
448            throws XmlPullParserException, IOException {
449        int drawableRes = 0;
450        int keyframeId = 0;
451
452        int j = 0;
453        final int numAttrs = attrs.getAttributeCount();
454        int[] states = new int[numAttrs];
455        for (int i = 0; i < numAttrs; i++) {
456            final int stateResId = attrs.getAttributeNameResource(i);
457            switch (stateResId) {
458                case 0:
459                    break;
460                case R.attr.id:
461                    keyframeId = attrs.getAttributeResourceValue(i, 0);
462                    break;
463                case R.attr.drawable:
464                    drawableRes = attrs.getAttributeResourceValue(i, 0);
465                    break;
466                default:
467                    final boolean hasState = attrs.getAttributeBooleanValue(i, false);
468                    states[j++] = hasState ? stateResId : -stateResId;
469            }
470        }
471        states = StateSet.trimStateSet(states, j);
472
473        final Drawable dr;
474        if (drawableRes != 0) {
475            dr = r.getDrawable(drawableRes);
476        } else {
477            int type;
478            while ((type = parser.next()) == XmlPullParser.TEXT) {
479            }
480            if (type != XmlPullParser.START_TAG) {
481                throw new XmlPullParserException(
482                        parser.getPositionDescription()
483                                + ": <item> tag requires a 'drawable' attribute or "
484                                + "child tag defining a drawable");
485            }
486            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
487        }
488
489        return mState.addStateSet(states, dr, keyframeId);
490    }
491
492    @Override
493    public Drawable mutate() {
494        if (!mMutated) {
495            final AnimatedStateListState newState = new AnimatedStateListState(mState, this, null);
496            setConstantState(newState);
497            mMutated = true;
498        }
499
500        return this;
501    }
502
503    private final AnimatorListenerAdapter mAnimListener = new AnimatorListenerAdapter() {
504        @Override
505        public void onAnimationEnd(Animator anim) {
506            selectDrawable(mTransitionToIndex);
507
508            mTransitionToIndex = -1;
509            mTransitionFromIndex = -1;
510            mTransition = null;
511        }
512    };
513
514    static class AnimatedStateListState extends StateListState {
515        private static final int REVERSE_SHIFT = 32;
516        private static final int REVERSE_MASK = 0x1;
517
518        final LongSparseLongArray mTransitions;
519        final SparseIntArray mStateIds;
520
521        AnimatedStateListState(@Nullable AnimatedStateListState orig,
522                @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) {
523            super(orig, owner, res);
524
525            if (orig != null) {
526                mTransitions = orig.mTransitions.clone();
527                mStateIds = orig.mStateIds.clone();
528            } else {
529                mTransitions = new LongSparseLongArray();
530                mStateIds = new SparseIntArray();
531            }
532        }
533
534        int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) {
535            final int pos = super.addChild(anim);
536            final long keyFromTo = generateTransitionKey(fromId, toId);
537            mTransitions.append(keyFromTo, pos);
538
539            if (reversible) {
540                final long keyToFrom = generateTransitionKey(toId, fromId);
541                mTransitions.append(keyToFrom, pos | (1L << REVERSE_SHIFT));
542            }
543
544            return addChild(anim);
545        }
546
547        int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
548            final int index = super.addStateSet(stateSet, drawable);
549            mStateIds.put(index, id);
550            return index;
551        }
552
553        int indexOfKeyframe(@NonNull int[] stateSet) {
554            final int index = super.indexOfStateSet(stateSet);
555            if (index >= 0) {
556                return index;
557            }
558
559            return super.indexOfStateSet(StateSet.WILD_CARD);
560        }
561
562        int getKeyframeIdAt(int index) {
563            return index < 0 ? 0 : mStateIds.get(index, 0);
564        }
565
566        int indexOfTransition(int fromId, int toId) {
567            final long keyFromTo = generateTransitionKey(fromId, toId);
568            return (int) mTransitions.get(keyFromTo, -1);
569        }
570
571        boolean isTransitionReversed(int fromId, int toId) {
572            final long keyFromTo = generateTransitionKey(fromId, toId);
573            return (mTransitions.get(keyFromTo, -1) >> REVERSE_SHIFT & REVERSE_MASK) == 1;
574        }
575
576        @Override
577        public Drawable newDrawable() {
578            return new AnimatedStateListDrawable(this, null);
579        }
580
581        @Override
582        public Drawable newDrawable(Resources res) {
583            return new AnimatedStateListDrawable(this, res);
584        }
585
586        private static long generateTransitionKey(int fromId, int toId) {
587            return (long) fromId << 32 | toId;
588        }
589    }
590
591    void setConstantState(@NonNull AnimatedStateListState state) {
592        super.setConstantState(state);
593
594        mState = state;
595    }
596
597    private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) {
598        super(null);
599
600        final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
601        setConstantState(newState);
602        onStateChange(getState());
603        jumpToCurrentState();
604    }
605
606    /**
607     * Interpolates between frames with respect to their individual durations.
608     */
609    private static class FrameInterpolator implements TimeInterpolator {
610        private int[] mFrameTimes;
611        private int mFrames;
612        private int mTotalDuration;
613
614        public FrameInterpolator(AnimationDrawable d, boolean reversed) {
615            updateFrames(d, reversed);
616        }
617
618        public int updateFrames(AnimationDrawable d, boolean reversed) {
619            final int N = d.getNumberOfFrames();
620            mFrames = N;
621
622            if (mFrameTimes == null || mFrameTimes.length < N) {
623                mFrameTimes = new int[N];
624            }
625
626            final int[] frameTimes = mFrameTimes;
627            int totalDuration = 0;
628            for (int i = 0; i < N; i++) {
629                final int duration = d.getDuration(reversed ? N - i - 1 : i);
630                frameTimes[i] = duration;
631                totalDuration += duration;
632            }
633
634            mTotalDuration = totalDuration;
635            return totalDuration;
636        }
637
638        public int getTotalDuration() {
639            return mTotalDuration;
640        }
641
642        @Override
643        public float getInterpolation(float input) {
644            final int elapsed = (int) (input * mTotalDuration + 0.5f);
645            final int N = mFrames;
646            final int[] frameTimes = mFrameTimes;
647
648            // Find the current frame and remaining time within that frame.
649            int remaining = elapsed;
650            int i = 0;
651            while (i < N && remaining >= frameTimes[i]) {
652                remaining -= frameTimes[i];
653                i++;
654            }
655
656            // Remaining time is relative of total duration.
657            final float frameElapsed;
658            if (i < N) {
659                frameElapsed = remaining / (float) mTotalDuration;
660            } else {
661                frameElapsed = 0;
662            }
663
664            return i / (float) N + frameElapsed;
665        }
666    }
667}
668