/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.graphics.drawable; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.Log; import android.util.LongSparseLongArray; import android.util.SparseIntArray; import android.util.StateSet; import com.android.internal.R; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; /** * Drawable containing a set of Drawable keyframes where the currently displayed * keyframe is chosen based on the current state set. Animations between * keyframes may optionally be defined using transition elements. * <p> * This drawable can be defined in an XML file with the <code> * <animated-selector></code> element. Each keyframe Drawable is defined in a * nested <code><item></code> element. Transitions are defined in a nested * <code><transition></code> element. * * @attr ref android.R.styleable#DrawableStates_state_focused * @attr ref android.R.styleable#DrawableStates_state_window_focused * @attr ref android.R.styleable#DrawableStates_state_enabled * @attr ref android.R.styleable#DrawableStates_state_checkable * @attr ref android.R.styleable#DrawableStates_state_checked * @attr ref android.R.styleable#DrawableStates_state_selected * @attr ref android.R.styleable#DrawableStates_state_activated * @attr ref android.R.styleable#DrawableStates_state_active * @attr ref android.R.styleable#DrawableStates_state_single * @attr ref android.R.styleable#DrawableStates_state_first * @attr ref android.R.styleable#DrawableStates_state_middle * @attr ref android.R.styleable#DrawableStates_state_last * @attr ref android.R.styleable#DrawableStates_state_pressed */ public class AnimatedStateListDrawable extends StateListDrawable { private static final String LOGTAG = AnimatedStateListDrawable.class.getSimpleName(); private static final String ELEMENT_TRANSITION = "transition"; private static final String ELEMENT_ITEM = "item"; private AnimatedStateListState mState; /** The currently running transition, if any. */ private Transition mTransition; /** Index to be set after the transition ends. */ private int mTransitionToIndex = -1; /** Index away from which we are transitioning. */ private int mTransitionFromIndex = -1; private boolean mMutated; public AnimatedStateListDrawable() { this(null, null); } @Override public boolean setVisible(boolean visible, boolean restart) { final boolean changed = super.setVisible(visible, restart); if (mTransition != null && (changed || restart)) { if (visible) { mTransition.start(); } else { // Ensure we're showing the correct state when visible. jumpToCurrentState(); } } return changed; } /** * Add a new drawable to the set of keyframes. * * @param stateSet An array of resource IDs to associate with the keyframe * @param drawable The drawable to show when in the specified state, may not be null * @param id The unique identifier for the keyframe */ public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { if (drawable == null) { throw new IllegalArgumentException("Drawable must not be null"); } mState.addStateSet(stateSet, drawable, id); onStateChange(getState()); } /** * Adds a new transition between keyframes. * * @param fromId Unique identifier of the starting keyframe * @param toId Unique identifier of the ending keyframe * @param transition An {@link Animatable} drawable to use as a transition, may not be null * @param reversible Whether the transition can be reversed */ public <T extends Drawable & Animatable> void addTransition(int fromId, int toId, @NonNull T transition, boolean reversible) { if (transition == null) { throw new IllegalArgumentException("Transition drawable must not be null"); } mState.addTransition(fromId, toId, transition, reversible); } @Override public boolean isStateful() { return true; } @Override protected boolean onStateChange(int[] stateSet) { // If we're not already at the target index, either attempt to find a // valid transition to it or jump directly there. final int targetIndex = mState.indexOfKeyframe(stateSet); boolean changed = targetIndex != getCurrentIndex() && (selectTransition(targetIndex) || selectDrawable(targetIndex)); // We need to propagate the state change to the current drawable, but // we can't call StateListDrawable.onStateChange() without changing the // current drawable. final Drawable current = getCurrent(); if (current != null) { changed |= current.setState(stateSet); } return changed; } private boolean selectTransition(int toIndex) { final int fromIndex; final Transition currentTransition = mTransition; if (currentTransition != null) { if (toIndex == mTransitionToIndex) { // Already animating to that keyframe. return true; } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) { // Reverse the current animation. currentTransition.reverse(); mTransitionToIndex = mTransitionFromIndex; mTransitionFromIndex = toIndex; return true; } // Start the next transition from the end of the current one. fromIndex = mTransitionToIndex; // Changing animation, end the current animation. currentTransition.stop(); } else { fromIndex = getCurrentIndex(); } // Reset state. mTransition = null; mTransitionFromIndex = -1; mTransitionToIndex = -1; final AnimatedStateListState state = mState; final int fromId = state.getKeyframeIdAt(fromIndex); final int toId = state.getKeyframeIdAt(toIndex); if (toId == 0 || fromId == 0) { // Missing a keyframe ID. return false; } final int transitionIndex = state.indexOfTransition(fromId, toId); if (transitionIndex < 0) { // Couldn't select a transition. return false; } boolean hasReversibleFlag = state.transitionHasReversibleFlag(fromId, toId); // This may fail if we're already on the transition, but that's okay! selectDrawable(transitionIndex); final Transition transition; final Drawable d = getCurrent(); if (d instanceof AnimationDrawable) { final boolean reversed = state.isTransitionReversed(fromId, toId); transition = new AnimationDrawableTransition((AnimationDrawable) d, reversed, hasReversibleFlag); } else if (d instanceof AnimatedVectorDrawable) { final boolean reversed = state.isTransitionReversed(fromId, toId); transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d, reversed, hasReversibleFlag); } else if (d instanceof Animatable) { transition = new AnimatableTransition((Animatable) d); } else { // We don't know how to animate this transition. return false; } transition.start(); mTransition = transition; mTransitionFromIndex = fromIndex; mTransitionToIndex = toIndex; return true; } private static abstract class Transition { public abstract void start(); public abstract void stop(); public void reverse() { // Not supported by default. } public boolean canReverse() { return false; } } private static class AnimatableTransition extends Transition { private final Animatable mA; public AnimatableTransition(Animatable a) { mA = a; } @Override public void start() { mA.start(); } @Override public void stop() { mA.stop(); } } private static class AnimationDrawableTransition extends Transition { private final ObjectAnimator mAnim; // Even AnimationDrawable is always reversible technically, but // we should obey the XML's android:reversible flag. private final boolean mHasReversibleFlag; public AnimationDrawableTransition(AnimationDrawable ad, boolean reversed, boolean hasReversibleFlag) { final int frameCount = ad.getNumberOfFrames(); final int fromFrame = reversed ? frameCount - 1 : 0; final int toFrame = reversed ? 0 : frameCount - 1; final FrameInterpolator interp = new FrameInterpolator(ad, reversed); final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame); anim.setAutoCancel(true); anim.setDuration(interp.getTotalDuration()); anim.setInterpolator(interp); mHasReversibleFlag = hasReversibleFlag; mAnim = anim; } @Override public boolean canReverse() { return mHasReversibleFlag; } @Override public void start() { mAnim.start(); } @Override public void reverse() { mAnim.reverse(); } @Override public void stop() { mAnim.cancel(); } } private static class AnimatedVectorDrawableTransition extends Transition { private final AnimatedVectorDrawable mAvd; // mReversed is indicating the current transition's direction. private final boolean mReversed; // mHasReversibleFlag is indicating whether the whole transition has // reversible flag set to true. // If mHasReversibleFlag is false, then mReversed is always false. private final boolean mHasReversibleFlag; public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd, boolean reversed, boolean hasReversibleFlag) { mAvd = avd; mReversed = reversed; mHasReversibleFlag = hasReversibleFlag; } @Override public boolean canReverse() { // When the transition's XML says it is not reversible, then we obey // it, even if the AVD itself is reversible. // This will help the single direction transition. return mAvd.canReverse() && mHasReversibleFlag; } @Override public void start() { if (mReversed) { reverse(); } else { mAvd.start(); } } @Override public void reverse() { if (canReverse()) { mAvd.reverse(); } else { Log.w(LOGTAG, "Can't reverse, either the reversible is set to false," + " or the AnimatedVectorDrawable can't reverse"); } } @Override public void stop() { mAvd.stop(); } } @Override public void jumpToCurrentState() { super.jumpToCurrentState(); if (mTransition != null) { mTransition.stop(); mTransition = null; selectDrawable(mTransitionToIndex); mTransitionToIndex = -1; mTransitionFromIndex = -1; } } @Override public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { final TypedArray a = obtainAttributes( r, theme, attrs, R.styleable.AnimatedStateListDrawable); super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible); updateStateFromTypedArray(a); updateDensity(r); a.recycle(); inflateChildElements(r, parser, attrs, theme); init(); } @Override public void applyTheme(@Nullable Theme theme) { super.applyTheme(theme); final AnimatedStateListState state = mState; if (state == null || state.mAnimThemeAttrs == null) { return; } final TypedArray a = theme.resolveAttributes( state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable); updateStateFromTypedArray(a); a.recycle(); init(); } private void updateStateFromTypedArray(TypedArray a) { final AnimatedStateListState state = mState; // Account for any configuration changes. state.mChangingConfigurations |= a.getChangingConfigurations(); // Extract the theme attributes, if any. state.mAnimThemeAttrs = a.extractThemeAttrs(); state.setVariablePadding(a.getBoolean( R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding)); state.setConstantSize(a.getBoolean( R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize)); state.setEnterFadeDuration(a.getInt( R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration)); state.setExitFadeDuration(a.getInt( R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration)); setDither(a.getBoolean( R.styleable.AnimatedStateListDrawable_dither, state.mDither)); setAutoMirrored(a.getBoolean( R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored)); } private void init() { onStateChange(getState()); } private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { int type; final int innerDepth = parser.getDepth() + 1; int depth; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { if (type != XmlPullParser.START_TAG) { continue; } if (depth > innerDepth) { continue; } if (parser.getName().equals(ELEMENT_ITEM)) { parseItem(r, parser, attrs, theme); } else if (parser.getName().equals(ELEMENT_TRANSITION)) { parseTransition(r, parser, attrs, theme); } } } private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { // This allows state list drawable item elements to be themed at // inflation time but does NOT make them work for Zygote preload. final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedStateListDrawableTransition); final int fromId = a.getResourceId( R.styleable.AnimatedStateListDrawableTransition_fromId, 0); final int toId = a.getResourceId( R.styleable.AnimatedStateListDrawableTransition_toId, 0); final boolean reversible = a.getBoolean( R.styleable.AnimatedStateListDrawableTransition_reversible, false); Drawable dr = a.getDrawable( R.styleable.AnimatedStateListDrawableTransition_drawable); a.recycle(); // Loading child elements modifies the state of the AttributeSet's // underlying parser, so it needs to happen after obtaining // attributes and extracting states. if (dr == null) { int type; while ((type = parser.next()) == XmlPullParser.TEXT) { } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException( parser.getPositionDescription() + ": <transition> tag requires a 'drawable' attribute or " + "child tag defining a drawable"); } dr = Drawable.createFromXmlInner(r, parser, attrs, theme); } return mState.addTransition(fromId, toId, dr, reversible); } private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { // This allows state list drawable item elements to be themed at // inflation time but does NOT make them work for Zygote preload. final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedStateListDrawableItem); final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0); Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable); a.recycle(); final int[] states = extractStateSet(attrs); // Loading child elements modifies the state of the AttributeSet's // underlying parser, so it needs to happen after obtaining // attributes and extracting states. if (dr == null) { int type; while ((type = parser.next()) == XmlPullParser.TEXT) { } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException( parser.getPositionDescription() + ": <item> tag requires a 'drawable' attribute or " + "child tag defining a drawable"); } dr = Drawable.createFromXmlInner(r, parser, attrs, theme); } return mState.addStateSet(states, dr, keyframeId); } @Override public Drawable mutate() { if (!mMutated && super.mutate() == this) { mState.mutate(); mMutated = true; } return this; } @Override AnimatedStateListState cloneConstantState() { return new AnimatedStateListState(mState, this, null); } /** * @hide */ public void clearMutated() { super.clearMutated(); mMutated = false; } static class AnimatedStateListState extends StateListState { // REVERSED_BIT is indicating the current transition's direction. private static final long REVERSED_BIT = 0x100000000l; // REVERSIBLE_FLAG_BIT is indicating whether the whole transition has // reversible flag set to true. private static final long REVERSIBLE_FLAG_BIT = 0x200000000l; int[] mAnimThemeAttrs; LongSparseLongArray mTransitions; SparseIntArray mStateIds; AnimatedStateListState(@Nullable AnimatedStateListState orig, @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) { super(orig, owner, res); if (orig != null) { // Perform a shallow copy and rely on mutate() to deep-copy. mAnimThemeAttrs = orig.mAnimThemeAttrs; mTransitions = orig.mTransitions; mStateIds = orig.mStateIds; } else { mTransitions = new LongSparseLongArray(); mStateIds = new SparseIntArray(); } } void mutate() { mTransitions = mTransitions.clone(); mStateIds = mStateIds.clone(); } int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) { final int pos = super.addChild(anim); final long keyFromTo = generateTransitionKey(fromId, toId); long reversibleBit = 0; if (reversible) { reversibleBit = REVERSIBLE_FLAG_BIT; } mTransitions.append(keyFromTo, pos | reversibleBit); if (reversible) { final long keyToFrom = generateTransitionKey(toId, fromId); mTransitions.append(keyToFrom, pos | REVERSED_BIT | reversibleBit); } return pos; } int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { final int index = super.addStateSet(stateSet, drawable); mStateIds.put(index, id); return index; } int indexOfKeyframe(@NonNull int[] stateSet) { final int index = super.indexOfStateSet(stateSet); if (index >= 0) { return index; } return super.indexOfStateSet(StateSet.WILD_CARD); } int getKeyframeIdAt(int index) { return index < 0 ? 0 : mStateIds.get(index, 0); } int indexOfTransition(int fromId, int toId) { final long keyFromTo = generateTransitionKey(fromId, toId); return (int) mTransitions.get(keyFromTo, -1); } boolean isTransitionReversed(int fromId, int toId) { final long keyFromTo = generateTransitionKey(fromId, toId); return (mTransitions.get(keyFromTo, -1) & REVERSED_BIT) != 0; } boolean transitionHasReversibleFlag(int fromId, int toId) { final long keyFromTo = generateTransitionKey(fromId, toId); return (mTransitions.get(keyFromTo, -1) & REVERSIBLE_FLAG_BIT) != 0; } @Override public boolean canApplyTheme() { return mAnimThemeAttrs != null || super.canApplyTheme(); } @Override public Drawable newDrawable() { return new AnimatedStateListDrawable(this, null); } @Override public Drawable newDrawable(Resources res) { return new AnimatedStateListDrawable(this, res); } private static long generateTransitionKey(int fromId, int toId) { return (long) fromId << 32 | toId; } } @Override protected void setConstantState(@NonNull DrawableContainerState state) { super.setConstantState(state); if (state instanceof AnimatedStateListState) { mState = (AnimatedStateListState) state; } } private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) { super(null); // Every animated state list drawable has its own constant state. final AnimatedStateListState newState = new AnimatedStateListState(state, this, res); setConstantState(newState); onStateChange(getState()); jumpToCurrentState(); } /** * Interpolates between frames with respect to their individual durations. */ private static class FrameInterpolator implements TimeInterpolator { private int[] mFrameTimes; private int mFrames; private int mTotalDuration; public FrameInterpolator(AnimationDrawable d, boolean reversed) { updateFrames(d, reversed); } public int updateFrames(AnimationDrawable d, boolean reversed) { final int N = d.getNumberOfFrames(); mFrames = N; if (mFrameTimes == null || mFrameTimes.length < N) { mFrameTimes = new int[N]; } final int[] frameTimes = mFrameTimes; int totalDuration = 0; for (int i = 0; i < N; i++) { final int duration = d.getDuration(reversed ? N - i - 1 : i); frameTimes[i] = duration; totalDuration += duration; } mTotalDuration = totalDuration; return totalDuration; } public int getTotalDuration() { return mTotalDuration; } @Override public float getInterpolation(float input) { final int elapsed = (int) (input * mTotalDuration + 0.5f); final int N = mFrames; final int[] frameTimes = mFrameTimes; // Find the current frame and remaining time within that frame. int remaining = elapsed; int i = 0; while (i < N && remaining >= frameTimes[i]) { remaining -= frameTimes[i]; i++; } // Remaining time is relative of total duration. final float frameElapsed; if (i < N) { frameElapsed = remaining / (float) mTotalDuration; } else { frameElapsed = 0; } return i / (float) N + frameElapsed; } } }