diff options
author | Alan Viverette <alanv@google.com> | 2014-05-09 13:03:12 -0700 |
---|---|---|
committer | Alan Viverette <alanv@google.com> | 2014-05-09 13:03:12 -0700 |
commit | 5e92c95d847a68178cf6099c801f82dcb7e4fa47 (patch) | |
tree | 2ec6ef3bdc9f227e5860949399ec834056dbf411 /graphics | |
parent | 3f4c72f42ba87846e73d1f1e77c16dba3991ec31 (diff) | |
download | frameworks_base-5e92c95d847a68178cf6099c801f82dcb7e4fa47.zip frameworks_base-5e92c95d847a68178cf6099c801f82dcb7e4fa47.tar.gz frameworks_base-5e92c95d847a68178cf6099c801f82dcb7e4fa47.tar.bz2 |
Animated state transition drawable
Change-Id: I6c795d55cd7ab9163fdd6fe6ce3771bbd7015d62
Diffstat (limited to 'graphics')
5 files changed, 561 insertions, 8 deletions
diff --git a/graphics/java/android/graphics/drawable/AnimatedStateListDrawable.java b/graphics/java/android/graphics/drawable/AnimatedStateListDrawable.java new file mode 100644 index 0000000..46e3401 --- /dev/null +++ b/graphics/java/android/graphics/drawable/AnimatedStateListDrawable.java @@ -0,0 +1,524 @@ +/* + * 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.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.util.AttributeSet; +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 ELEMENT_TRANSITION = "transition"; + private static final String ELEMENT_ITEM = "item"; + + private AnimatedStateListState mState; + + /** The currently running animation, if any. */ + private ObjectAnimator mAnim; + + /** Index to be set after the animation ends. */ + private int mAnimToIndex = -1; + + /** Index away from which we are animating. */ + private int mAnimFromIndex = -1; + + private boolean mMutated; + + public AnimatedStateListDrawable() { + this(null, null); + } + + /** + * 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 + * @param id The unique identifier for the keyframe + */ + public void addState(int[] stateSet, Drawable drawable, int id) { + if (drawable != 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 anim An AnimationDrawable to use as a transition + * @param reversible Whether the transition can be reversed + */ + public void addTransition(int fromId, int toId, AnimationDrawable anim, boolean reversible) { + mState.addTransition(fromId, toId, anim, reversible); + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected boolean onStateChange(int[] stateSet) { + final int keyframeIndex = mState.indexOfKeyframe(stateSet); + if (keyframeIndex == getCurrentIndex()) { + return false; + } + + if (selectTransition(keyframeIndex)) { + return true; + } + + if (selectDrawable(keyframeIndex)) { + return true; + } + + return super.onStateChange(stateSet); + } + + private boolean selectTransition(int toIndex) { + if (mAnim != null) { + if (toIndex == mAnimToIndex) { + // Already animating to that keyframe. + return true; + } else if (toIndex == mAnimFromIndex) { + // Reverse the current animation. + mAnim.reverse(); + mAnimFromIndex = mAnimToIndex; + mAnimToIndex = toIndex; + return true; + } + + // Changing animation, end the current animation. + mAnim.end(); + } + + final AnimatedStateListState state = mState; + final int fromIndex = getCurrentIndex(); + 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 || !selectDrawable(transitionIndex)) { + // Couldn't select a transition. + return false; + } + + final Drawable d = getCurrent(); + if (!(d instanceof AnimationDrawable)) { + // Transition isn't an animation. + return false; + } + + final AnimationDrawable ad = (AnimationDrawable) d; + final boolean reversed = mState.isTransitionReversed(fromId, toId); + 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.addListener(mAnimListener); + anim.setInterpolator(interp); + anim.start(); + + mAnim = anim; + mAnimFromIndex = fromIndex; + mAnimToIndex = toIndex; + return true; + } + + @Override + public void jumpToCurrentState() { + super.jumpToCurrentState(); + + if (mAnim != null) { + mAnim.end(); + } + } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) + throws XmlPullParserException, IOException { + final TypedArray a = r.obtainAttributes(attrs, R.styleable.AnimatedStateListDrawable); + + super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible); + + final StateListState stateListState = getStateListState(); + stateListState.setVariablePadding(a.getBoolean( + R.styleable.AnimatedStateListDrawable_variablePadding, false)); + stateListState.setConstantSize(a.getBoolean( + R.styleable.AnimatedStateListDrawable_constantSize, false)); + stateListState.setEnterFadeDuration(a.getInt( + R.styleable.AnimatedStateListDrawable_enterFadeDuration, 0)); + stateListState.setExitFadeDuration(a.getInt( + R.styleable.AnimatedStateListDrawable_exitFadeDuration, 0)); + + setDither(a.getBoolean(R.styleable.AnimatedStateListDrawable_dither, true)); + setAutoMirrored(a.getBoolean(R.styleable.AnimatedStateListDrawable_autoMirrored, false)); + + a.recycle(); + + 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); + } + } + + onStateChange(getState()); + } + + private int parseTransition(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) + throws XmlPullParserException, IOException { + int drawableRes = 0; + int fromId = 0; + int toId = 0; + boolean reversible = false; + + final int numAttrs = attrs.getAttributeCount(); + for (int i = 0; i < numAttrs; i++) { + final int stateResId = attrs.getAttributeNameResource(i); + switch (stateResId) { + case 0: + break; + case R.attr.fromId: + fromId = attrs.getAttributeResourceValue(i, 0); + break; + case R.attr.toId: + toId = attrs.getAttributeResourceValue(i, 0); + break; + case R.attr.drawable: + drawableRes = attrs.getAttributeResourceValue(i, 0); + break; + case R.attr.reversible: + reversible = attrs.getAttributeBooleanValue(i, false); + break; + } + } + + final Drawable dr; + if (drawableRes != 0) { + dr = r.getDrawable(drawableRes); + } else { + 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.createFromXmlInnerThemed(r, parser, attrs, theme); + } + + final AnimationDrawable anim; + if (dr instanceof AnimationDrawable) { + anim = (AnimationDrawable) dr; + } else { + throw new XmlPullParserException(parser.getPositionDescription() + + ": <transition> tag requires a 'drawable' attribute or " + + "child tag defining a drawable of type <animation>"); + } + + return mState.addTransition(fromId, toId, anim, reversible); + } + + private int parseItem(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) + throws XmlPullParserException, IOException { + int drawableRes = 0; + int keyframeId = 0; + + int j = 0; + final int numAttrs = attrs.getAttributeCount(); + int[] states = new int[numAttrs]; + for (int i = 0; i < numAttrs; i++) { + final int stateResId = attrs.getAttributeNameResource(i); + switch (stateResId) { + case 0: + break; + case R.attr.id: + keyframeId = attrs.getAttributeResourceValue(i, 0); + break; + case R.attr.drawable: + drawableRes = attrs.getAttributeResourceValue(i, 0); + break; + default: + final boolean hasState = attrs.getAttributeBooleanValue(i, false); + states[j++] = hasState ? stateResId : -stateResId; + } + } + states = StateSet.trimStateSet(states, j); + + final Drawable dr; + if (drawableRes != 0) { + dr = r.getDrawable(drawableRes); + } else { + 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.createFromXmlInnerThemed(r, parser, attrs, theme); + } + + return mState.addStateSet(states, dr, keyframeId); + } + + @Override + public Drawable mutate() { + if (!mMutated) { + final AnimatedStateListState newState = new AnimatedStateListState(mState, this, null); + setConstantState(newState); + mMutated = true; + } + + return this; + } + + private final AnimatorListenerAdapter mAnimListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator anim) { + selectDrawable(mAnimToIndex); + + mAnimToIndex = -1; + mAnimFromIndex = -1; + mAnim = null; + } + }; + + static class AnimatedStateListState extends StateListState { + private static final int REVERSE_SHIFT = 32; + private static final int REVERSE_MASK = 0x1; + + final LongSparseLongArray mTransitions; + final SparseIntArray mStateIds; + + AnimatedStateListState(AnimatedStateListState orig, AnimatedStateListDrawable owner, + Resources res) { + super(orig, owner, res); + + if (orig != null) { + mTransitions = orig.mTransitions.clone(); + mStateIds = orig.mStateIds.clone(); + } else { + mTransitions = new LongSparseLongArray(); + mStateIds = new SparseIntArray(); + } + } + + int addTransition(int fromId, int toId, AnimationDrawable anim, boolean reversible) { + final int pos = super.addChild(anim); + final long keyFromTo = generateTransitionKey(fromId, toId); + mTransitions.append(keyFromTo, pos); + + if (reversible) { + final long keyToFrom = generateTransitionKey(toId, fromId); + mTransitions.append(keyToFrom, pos | (1L << REVERSE_SHIFT)); + } + + return addChild(anim); + } + + int addStateSet(int[] stateSet, Drawable drawable, int id) { + final int index = super.addStateSet(stateSet, drawable); + mStateIds.put(index, id); + return index; + } + + int indexOfKeyframe(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) >> REVERSE_SHIFT & REVERSE_MASK) == 1; + } + + @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; + } + } + + void setConstantState(AnimatedStateListState state) { + super.setConstantState(state); + + mState = state; + } + + private AnimatedStateListDrawable(AnimatedStateListState state, Resources res) { + super(null); + + 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; + } + } +} + diff --git a/graphics/java/android/graphics/drawable/AnimationDrawable.java b/graphics/java/android/graphics/drawable/AnimationDrawable.java index 3f94e26..da4bc10 100644 --- a/graphics/java/android/graphics/drawable/AnimationDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimationDrawable.java @@ -94,7 +94,7 @@ public class AnimationDrawable extends DrawableContainer implements Runnable, An boolean changed = super.setVisible(visible, restart); if (visible) { if (changed || restart) { - setFrame(0, true, true); + setFrame(0, true, mCurFrame >= 0); } } else { unscheduleSelf(this); diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java index b9d5e19..b939636 100644 --- a/graphics/java/android/graphics/drawable/Drawable.java +++ b/graphics/java/android/graphics/drawable/Drawable.java @@ -1039,6 +1039,8 @@ public abstract class Drawable { final String name = parser.getName(); if (name.equals("selector")) { drawable = new StateListDrawable(); + } else if (name.equals("animated-selector")) { + drawable = new AnimatedStateListDrawable(); } else if (name.equals("level-list")) { drawable = new LevelListDrawable(); } else if (name.equals("layer-list")) { diff --git a/graphics/java/android/graphics/drawable/DrawableContainer.java b/graphics/java/android/graphics/drawable/DrawableContainer.java index 1f8b51d..08fc99d 100644 --- a/graphics/java/android/graphics/drawable/DrawableContainer.java +++ b/graphics/java/android/graphics/drawable/DrawableContainer.java @@ -359,6 +359,16 @@ public class DrawableContainer extends Drawable implements Drawable.Callback { mDrawableContainerState.getOpacity(); } + /** @hide */ + public void setCurrentIndex(int index) { + selectDrawable(index); + } + + /** @hide */ + public int getCurrentIndex() { + return mCurIndex; + } + public boolean selectDrawable(int idx) { if (idx == mCurIndex) { return false; diff --git a/graphics/java/android/graphics/drawable/StateListDrawable.java b/graphics/java/android/graphics/drawable/StateListDrawable.java index 271af2b..f22a063 100644 --- a/graphics/java/android/graphics/drawable/StateListDrawable.java +++ b/graphics/java/android/graphics/drawable/StateListDrawable.java @@ -55,8 +55,9 @@ import android.util.StateSet; * @attr ref android.R.styleable#DrawableStates_state_pressed */ public class StateListDrawable extends DrawableContainer { + private static final String TAG = StateListDrawable.class.getSimpleName(); + private static final boolean DEBUG = false; - private static final String TAG = "StateListDrawable"; /** * To be proper, we should have a getter for dither (and alpha, etc.) @@ -69,7 +70,8 @@ public class StateListDrawable extends DrawableContainer { * to improve the quality at negligible cost. */ private static final boolean DEFAULT_DITHER = true; - private final StateListState mStateListState; + + private StateListState mStateListState; private boolean mMutated; public StateListDrawable() { @@ -274,7 +276,7 @@ public class StateListDrawable extends DrawableContainer { mStateListState.setLayoutDirection(layoutDirection); } - static final class StateListState extends DrawableContainerState { + static class StateListState extends DrawableContainerState { int[][] mStateSets; StateListState(StateListState orig, StateListDrawable owner, Resources res) { @@ -293,7 +295,7 @@ public class StateListDrawable extends DrawableContainer { return pos; } - private int indexOfStateSet(int[] stateSet) { + int indexOfStateSet(int[] stateSet) { final int[][] stateSets = mStateSets; final int N = getChildCount(); for (int i = 0; i < N; i++) { @@ -323,11 +325,26 @@ public class StateListDrawable extends DrawableContainer { } } + void setConstantState(StateListState state) { + super.setConstantState(state); + + mStateListState = state; + } + private StateListDrawable(StateListState state, Resources res) { - StateListState as = new StateListState(state, this, res); - mStateListState = as; - setConstantState(as); + final StateListState newState = new StateListState(state, this, res); + setConstantState(newState); onStateChange(getState()); } + + /** + * This constructor exists so subclasses can avoid calling the default + * constructor and setting up a StateListDrawable-specific constant state. + */ + StateListDrawable(StateListState state) { + if (state != null) { + setConstantState(state); + } + } } |