/*
* 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;
}
}
}