summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Doris Liu <tianliu@google.com> 2017-01-25 17:22:57 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2017-01-25 17:23:00 +0000
commit32eaa6722cd37406bfabcf07483c0e17f9513277 (patch)
treeec7ed14d44bfc50c7e36a59add748eb2a9f7c391
parent617bbb3ba4bab4e0c3d3c18b160b29d89c84f3ba (diff)
parent13351997aa36eb53e5ff0fcee3f5e3da83787278 (diff)
Merge "New functionalities for AnimatorSet: Reverse, Seek"
-rw-r--r--api/current.txt4
-rw-r--r--api/system-current.txt4
-rw-r--r--api/test-current.txt4
-rw-r--r--core/java/android/animation/AnimationHandler.java3
-rw-r--r--core/java/android/animation/Animator.java93
-rw-r--r--core/java/android/animation/AnimatorSet.java970
-rw-r--r--core/java/android/animation/ObjectAnimator.java5
-rw-r--r--core/java/android/animation/ValueAnimator.java215
8 files changed, 996 insertions, 302 deletions
diff --git a/api/current.txt b/api/current.txt
index 18e4adfc70e6..fe9d8d08c457 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3068,8 +3068,10 @@ package android.animation {
public static abstract interface Animator.AnimatorListener {
method public abstract void onAnimationCancel(android.animation.Animator);
+ method public default void onAnimationEnd(android.animation.Animator, boolean);
method public abstract void onAnimationEnd(android.animation.Animator);
method public abstract void onAnimationRepeat(android.animation.Animator);
+ method public default void onAnimationStart(android.animation.Animator, boolean);
method public abstract void onAnimationStart(android.animation.Animator);
}
@@ -3105,6 +3107,8 @@ package android.animation {
method public void playSequentially(java.util.List<android.animation.Animator>);
method public void playTogether(android.animation.Animator...);
method public void playTogether(java.util.Collection<android.animation.Animator>);
+ method public void reverse();
+ method public void setCurrentPlayTime(long);
method public android.animation.AnimatorSet setDuration(long);
method public void setInterpolator(android.animation.TimeInterpolator);
method public void setStartDelay(long);
diff --git a/api/system-current.txt b/api/system-current.txt
index a192c9265be3..b55bd60bd76c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -3187,8 +3187,10 @@ package android.animation {
public static abstract interface Animator.AnimatorListener {
method public abstract void onAnimationCancel(android.animation.Animator);
+ method public default void onAnimationEnd(android.animation.Animator, boolean);
method public abstract void onAnimationEnd(android.animation.Animator);
method public abstract void onAnimationRepeat(android.animation.Animator);
+ method public default void onAnimationStart(android.animation.Animator, boolean);
method public abstract void onAnimationStart(android.animation.Animator);
}
@@ -3224,6 +3226,8 @@ package android.animation {
method public void playSequentially(java.util.List<android.animation.Animator>);
method public void playTogether(android.animation.Animator...);
method public void playTogether(java.util.Collection<android.animation.Animator>);
+ method public void reverse();
+ method public void setCurrentPlayTime(long);
method public android.animation.AnimatorSet setDuration(long);
method public void setInterpolator(android.animation.TimeInterpolator);
method public void setStartDelay(long);
diff --git a/api/test-current.txt b/api/test-current.txt
index 47440e6637fb..950c3d7d29a8 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -3068,8 +3068,10 @@ package android.animation {
public static abstract interface Animator.AnimatorListener {
method public abstract void onAnimationCancel(android.animation.Animator);
+ method public default void onAnimationEnd(android.animation.Animator, boolean);
method public abstract void onAnimationEnd(android.animation.Animator);
method public abstract void onAnimationRepeat(android.animation.Animator);
+ method public default void onAnimationStart(android.animation.Animator, boolean);
method public abstract void onAnimationStart(android.animation.Animator);
}
@@ -3105,6 +3107,8 @@ package android.animation {
method public void playSequentially(java.util.List<android.animation.Animator>);
method public void playTogether(android.animation.Animator...);
method public void playTogether(java.util.Collection<android.animation.Animator>);
+ method public void reverse();
+ method public void setCurrentPlayTime(long);
method public android.animation.AnimatorSet setDuration(long);
method public void setInterpolator(android.animation.TimeInterpolator);
method public void setStartDelay(long);
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index 95262ab46a9e..e2e5a8f66288 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -276,8 +276,9 @@ public class AnimationHandler {
* Run animation based on the frame time.
* @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
* base.
+ * @return if the animation has finished.
*/
- void doAnimationFrame(long frameTime);
+ boolean doAnimationFrame(long frameTime);
/**
* This notifies the callback of frame commit time. Frame commit time is the time after
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index c51725a2caac..634dc1fd6ee7 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -26,7 +26,7 @@ import java.util.ArrayList;
* This is the superclass for classes which provide basic support for animations which can be
* started, ended, and have <code>AnimatorListeners</code> added to them.
*/
-public abstract class Animator implements Cloneable {
+public abstract class Animator implements Cloneable, AnimationHandler.AnimationFrameCallback {
/**
* The value used to indicate infinite duration (e.g. when Animators repeat infinitely).
@@ -465,11 +465,102 @@ public abstract class Animator implements Cloneable {
}
/**
+ * @hide
+ */
+ @Override
+ public boolean doAnimationFrame(long frameTime) {
+ // TODO: Need to find a better signal than this
+ return getDuration() + getStartDelay() >= frameTime;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void commitAnimationFrame(long frameTime) {}
+
+
+ /**
+ * Internal use only.
+ * This call starts the animation in regular or reverse direction without requiring them to
+ * register frame callbacks. The caller will be responsible for all the subsequent animation
+ * pulses. Specifically, the caller needs to call doAnimationFrame(...) for the animation on
+ * every frame.
+ *
+ * @param inReverse whether the animation should play in reverse direction
+ */
+ void startWithoutPulsing(boolean inReverse) {
+ if (inReverse) {
+ reverse();
+ } else {
+ start();
+ }
+ }
+
+ /**
+ * Internal use only.
+ * Skips the animation value to end/start, depending on whether the play direction is forward
+ * or backward.
+ *
+ * @param inReverse whether the end value is based on a reverse direction. If yes, this is
+ * equivalent to skip to start value in a forward playing direction.
+ */
+ void skipToEndValue(boolean inReverse) {}
+
+
+ /**
+ * Internal use only.
+ *
+ * Returns whether the animation has start/end values setup. For most of the animations, this
+ * should always be true. For ObjectAnimators, the start values are setup in the initialization
+ * of the animation.
+ */
+ boolean isInitialized() {
+ return true;
+ }
+
+ /**
+ * Internal use only.
+ */
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {}
+
+ /**
* <p>An animation listener receives notifications from an animation.
* Notifications indicate animation related events, such as the end or the
* repetition of the animation.</p>
*/
public static interface AnimatorListener {
+
+ /**
+ * <p>Notifies the start of the animation as well as the animation's overall play direction.
+ * This method's default behavior is to call {@link #onAnimationStart(Animator)}. This
+ * method can be overridden, though not required, to get the additional play direction info
+ * when an animation starts. Skipping calling super when overriding this method results in
+ * {@link #onAnimationStart(Animator)} not getting called.
+ *
+ * @param animation The started animation.
+ * @param isReverse Whether the animation is playing in reverse.
+ */
+ default void onAnimationStart(Animator animation, boolean isReverse) {
+ onAnimationStart(animation);
+ }
+
+ /**
+ * <p>Notifies the end of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.</p>
+ *
+ * <p>This method's default behavior is to call {@link #onAnimationEnd(Animator)}. This
+ * method can be overridden, though not required, to get the additional play direction info
+ * when an animation ends. Skipping calling super when overriding this method results in
+ * {@link #onAnimationEnd(Animator)} not getting called.
+ *
+ * @param animation The animation which reached its end.
+ * @param isReverse Whether the animation is playing in reverse.
+ */
+ default void onAnimationEnd(Animator animation, boolean isReverse) {
+ onAnimationEnd(animation);
+ }
+
/**
* <p>Notifies the start of the animation.</p>
*
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index a904f9d5447b..d5814a30ac17 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -19,11 +19,15 @@ package android.animation;
import android.app.ActivityThread;
import android.app.Application;
import android.os.Build;
+import android.os.Looper;
+import android.util.AndroidRuntimeException;
import android.util.ArrayMap;
import android.util.Log;
+import android.view.animation.Animation;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Comparator;
import java.util.List;
/**
@@ -52,7 +56,7 @@ import java.util.List;
* Animation</a> developer guide.</p>
* </div>
*/
-public final class AnimatorSet extends Animator {
+public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {
private static final String TAG = "AnimatorSet";
/**
@@ -66,7 +70,7 @@ public final class AnimatorSet extends Animator {
* Tracks animations currently being played, so that we know what to
* cancel or end when cancel() or end() is called on this AnimatorSet
*/
- private ArrayList<Animator> mPlayingSet = new ArrayList<Animator>();
+ private ArrayList<Node> mPlayingSet = new ArrayList<Node>();
/**
* Contains all nodes, mapped to their respective Animators. When new
@@ -77,6 +81,11 @@ public final class AnimatorSet extends Animator {
private ArrayMap<Animator, Node> mNodeMap = new ArrayMap<Animator, Node>();
/**
+ * Contains the start and end events of all the nodes. All these events are sorted in this list.
+ */
+ private ArrayList<AnimationEvent> mEvents = new ArrayList<>();
+
+ /**
* Set of all nodes created for this AnimatorSet. This list is used upon
* starting the set, and the nodes are placed in sorted order into the
* sortedNodes collection.
@@ -84,21 +93,6 @@ public final class AnimatorSet extends Animator {
private ArrayList<Node> mNodes = new ArrayList<Node>();
/**
- * Animator Listener that tracks the lifecycle of each Animator in the set. It will be added
- * to each Animator before they start and removed after they end.
- */
- private AnimatorSetListener mSetListener = new AnimatorSetListener(this);
-
- /**
- * Flag indicating that the AnimatorSet has been manually
- * terminated (by calling cancel() or end()).
- * This flag is used to avoid starting other animations when currently-playing
- * child animations of this AnimatorSet end. It also determines whether cancel/end
- * notifications are sent out via the normal AnimatorSetListener mechanism.
- */
- private boolean mTerminated = false;
-
- /**
* Tracks whether any change has been made to the AnimatorSet, which is then used to
* determine whether the dependency graph should be re-constructed.
*/
@@ -131,8 +125,6 @@ public final class AnimatorSet extends Animator {
// was set on this AnimatorSet, so it should not be passed down to the children.
private TimeInterpolator mInterpolator = null;
- // Whether the AnimatorSet can be reversed.
- private boolean mReversible = true;
// The total duration of finishing all the Animators in the set.
private long mTotalDuration = 0;
@@ -142,6 +134,46 @@ public final class AnimatorSet extends Animator {
// the animator set and immediately end it for N and forward.
private final boolean mShouldIgnoreEndWithoutStart;
+ // In pre-O releases, calling start() doesn't reset all the animators values to start values.
+ // As a result, the start of the animation is inconsistent with what setCurrentPlayTime(0) would
+ // look like on O. Also it is inconsistent with what reverse() does on O, as reverse would
+ // advance all the animations to the right beginning values for before starting to reverse.
+ // From O and forward, we will add an additional step of resetting the animation values (unless
+ // the animation was previously seeked and therefore doesn't start from the beginning).
+ private final boolean mShouldResetValuesAtStart;
+
+ // The time, in milliseconds, when last frame of the animation came in. -1 when the animation is
+ // not running.
+ private long mLastFrameTime = -1;
+
+ // The time, in milliseconds, when the first frame of the animation came in.
+ // -1 when the animation is not running.
+ private long mFirstFrame = -1;
+
+ // The time, in milliseconds, when the first frame of the animation came in.
+ // -1 when the animation is not running.
+ private int mLastEventId = -1;
+
+ // Indicates whether the animation is reversing.
+ private boolean mReversing = false;
+
+ // Indicates whether the animation should register frame callbacks. If false, the animation will
+ // passively wait for an AnimatorSet to pulse it.
+ private boolean mSelfPulse = true;
+
+ // SeekState stores the last seeked play time as well as seek direction.
+ private SeekState mSeekState = new SeekState();
+
+ // Indicates where children animators are all initialized with their start values captured.
+ private boolean mChildrenInitialized = false;
+
+ /**
+ * Set on the next frame after pause() is called, used to calculate a new startTime
+ * or delayStartTime which allows the animator set to continue from the point at which
+ * it was paused. If negative, has not yet been set.
+ */
+ private long mPauseTime = -1;
+
public AnimatorSet() {
super();
mNodeMap.put(mDelayAnim, mRootNode);
@@ -150,10 +182,19 @@ public final class AnimatorSet extends Animator {
Application app = ActivityThread.currentApplication();
if (app == null || app.getApplicationInfo() == null) {
mShouldIgnoreEndWithoutStart = true;
- } else if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
- mShouldIgnoreEndWithoutStart = true;
+ mShouldResetValuesAtStart = false;
} else {
- mShouldIgnoreEndWithoutStart = false;
+ if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+ mShouldIgnoreEndWithoutStart = true;
+ } else {
+ mShouldIgnoreEndWithoutStart = false;
+ }
+
+ if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O) {
+ mShouldResetValuesAtStart = false;
+ } else {
+ mShouldResetValuesAtStart = true;
+ }
}
}
@@ -206,7 +247,6 @@ public final class AnimatorSet extends Animator {
if (items.length == 1) {
play(items[0]);
} else {
- mReversible = false;
for (int i = 0; i < items.length - 1; ++i) {
play(items[i]).before(items[i + 1]);
}
@@ -225,7 +265,6 @@ public final class AnimatorSet extends Animator {
if (items.size() == 1) {
play(items.get(0));
} else {
- mReversible = false;
for (int i = 0; i < items.size() - 1; ++i) {
play(items.get(i)).before(items.get(i + 1));
}
@@ -350,7 +389,9 @@ public final class AnimatorSet extends Animator {
@SuppressWarnings("unchecked")
@Override
public void cancel() {
- mTerminated = true;
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
if (isStarted()) {
ArrayList<AnimatorListener> tmpListeners = null;
if (mListeners != null) {
@@ -360,18 +401,13 @@ public final class AnimatorSet extends Animator {
tmpListeners.get(i).onAnimationCancel(this);
}
}
- ArrayList<Animator> playingSet = new ArrayList<>(mPlayingSet);
+ ArrayList<Node> playingSet = new ArrayList<>(mPlayingSet);
int setSize = playingSet.size();
for (int i = 0; i < setSize; i++) {
- playingSet.get(i).cancel();
+ playingSet.get(i).mAnimation.cancel();
}
- if (tmpListeners != null) {
- int size = tmpListeners.size();
- for (int i = 0; i < size; i++) {
- tmpListeners.get(i).onAnimationEnd(this);
- }
- }
- mStarted = false;
+ mPlayingSet.clear();
+ endAnimation();
}
}
@@ -383,50 +419,40 @@ public final class AnimatorSet extends Animator {
*/
@Override
public void end() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
if (mShouldIgnoreEndWithoutStart && !isStarted()) {
return;
}
- mTerminated = true;
if (isStarted()) {
- endRemainingAnimations();
- }
- if (mListeners != null) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- for (int i = 0; i < tmpListeners.size(); i++) {
- tmpListeners.get(i).onAnimationEnd(this);
- }
- }
- mStarted = false;
- }
-
- /**
- * Iterate the animations that haven't finished or haven't started, and end them.
- */
- private void endRemainingAnimations() {
- ArrayList<Animator> remainingList = new ArrayList<Animator>(mNodes.size());
- remainingList.addAll(mPlayingSet);
-
- int index = 0;
- while (index < remainingList.size()) {
- Animator anim = remainingList.get(index);
- anim.end();
- index++;
- Node node = mNodeMap.get(anim);
- if (node.mChildNodes != null) {
- int childSize = node.mChildNodes.size();
- for (int i = 0; i < childSize; i++) {
- Node child = node.mChildNodes.get(i);
- if (child.mLatestParent != node) {
- continue;
+ // Iterate the animations that haven't finished or haven't started, and end them.
+ if (mReversing) {
+ // Between start() and first frame, mLastEventId would be unset (i.e. -1)
+ mLastEventId = mLastEventId == -1 ? mEvents.size() : mLastEventId;
+ for (int j = mLastEventId - 1; j >= 0; j--) {
+ AnimationEvent event = mEvents.get(j);
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ event.mNode.mAnimation.reverse();
+ } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ event.mNode.mAnimation.end();
+ }
+ }
+ } else {
+ for (int j = mLastEventId + 1; j < mEvents.size(); j++) {
+ AnimationEvent event = mEvents.get(j);
+ if (event.mEvent == AnimationEvent.ANIMATION_START) {
+ event.mNode.mAnimation.start();
+ } else if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ event.mNode.mAnimation.end();
}
- remainingList.add(child.mAnimation);
}
}
+ mPlayingSet.clear();
}
+ endAnimation();
}
-
/**
* Returns true if any of the child animations of this AnimatorSet have been started and have
* not yet ended. Child animations will not be started until the AnimatorSet has gone past
@@ -437,14 +463,12 @@ public final class AnimatorSet extends Animator {
*/
@Override
public boolean isRunning() {
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- if (node != mRootNode && node.mAnimation.isStarted()) {
- return true;
- }
+ if (mStartDelay > 0) {
+ return mStarted && !mDelayAnim.isRunning();
+ } else {
+ // No start delay, animation should start right away
+ return mStarted;
}
- return false;
}
@Override
@@ -482,9 +506,6 @@ public final class AnimatorSet extends Animator {
return;
}
mStartDelay = startDelay;
- if (mStartDelay > 0) {
- mReversible = false;
- }
if (!mDependencyDirty) {
// Dependency graph already constructed, update all the nodes' start/end time
int size = mNodes.size();
@@ -562,40 +583,26 @@ public final class AnimatorSet extends Animator {
@Override
public void pause() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
boolean previouslyPaused = mPaused;
super.pause();
if (!previouslyPaused && mPaused) {
- if (mDelayAnim.isStarted()) {
- // If delay hasn't passed, pause the start delay animator.
- mDelayAnim.pause();
- } else {
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- if (node != mRootNode) {
- node.mAnimation.pause();
- }
- }
- }
+ mPauseTime = -1;
}
}
@Override
public void resume() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
boolean previouslyPaused = mPaused;
super.resume();
if (previouslyPaused && !mPaused) {
- if (mDelayAnim.isStarted()) {
- // If start delay hasn't passed, resume the previously paused start delay animator
- mDelayAnim.resume();
- } else {
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- if (node != mRootNode) {
- node.mAnimation.resume();
- }
- }
+ if (mPauseTime >= 0) {
+ addAnimationCallback(0);
}
}
}
@@ -610,9 +617,33 @@ public final class AnimatorSet extends Animator {
@SuppressWarnings("unchecked")
@Override
public void start() {
- mTerminated = false;
+ start(false, true);
+ }
+
+ @Override
+ void startWithoutPulsing(boolean inReverse) {
+ start(inReverse, false);
+ }
+
+ private void initAnimation() {
+ if (mInterpolator != null) {
+ for (int i = 0; i < mNodes.size(); i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.setInterpolator(mInterpolator);
+ }
+ }
+ updateAnimatorsDuration();
+ createDependencyGraph();
+ }
+
+ private void start(boolean inReverse, boolean selfPulse) {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
mStarted = true;
+ mSelfPulse = selfPulse;
mPaused = false;
+ mPauseTime = -1;
int size = mNodes.size();
for (int i = 0; i < size; i++) {
@@ -621,26 +652,17 @@ public final class AnimatorSet extends Animator {
node.mAnimation.setAllowRunningAsynchronously(false);
}
- if (mInterpolator != null) {
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- node.mAnimation.setInterpolator(mInterpolator);
- }
+ initAnimation();
+ if (inReverse && !canReverse()) {
+ throw new UnsupportedOperationException("Cannot reverse infinite AnimatorSet");
}
- updateAnimatorsDuration();
- createDependencyGraph();
+ mReversing = inReverse;
// Now that all dependencies are set up, start the animations that should be started.
- boolean setIsEmpty = false;
- if (mStartDelay > 0) {
- start(mRootNode);
- } else if (isEmptySet(this)) {
- // Set is empty or contains only empty animator sets. Skip to end in this case.
- setIsEmpty = true;
- } else {
- // No delay, but there are other animators in the set
- onChildAnimatorEnded(mDelayAnim);
+ boolean setIsEmpty = isEmptySet(this);
+ if (!setIsEmpty) {
+ startAnimation();
}
if (mListeners != null) {
@@ -648,12 +670,12 @@ public final class AnimatorSet extends Animator {
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationStart(this);
+ tmpListeners.get(i).onAnimationStart(this, inReverse);
}
}
if (setIsEmpty) {
// In the case of empty AnimatorSet, we will trigger the onAnimationEnd() right away.
- onChildAnimatorEnded(mDelayAnim);
+ end();
}
}
@@ -690,11 +712,419 @@ public final class AnimatorSet extends Animator {
mDelayAnim.setDuration(mStartDelay);
}
- void start(final Node node) {
- final Animator anim = node.mAnimation;
- mPlayingSet.add(anim);
- anim.addListener(mSetListener);
- anim.start();
+ @Override
+ void skipToEndValue(boolean inReverse) {
+ if (!isInitialized()) {
+ throw new UnsupportedOperationException("Children must be initialized.");
+ }
+
+ // This makes sure the animation events are sorted an up to date.
+ initAnimation();
+
+ // Calling skip to the end in the sequence that they would be called in a forward/reverse
+ // run, such that the sequential animations modifying the same property would have
+ // the right value in the end.
+ if (inReverse) {
+ for (int i = mEvents.size() - 1; i >= 0; i--) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ mEvents.get(i).mNode.mAnimation.skipToEndValue(true);
+ }
+ }
+ } else {
+ for (int i = 0; i < mEvents.size(); i++) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_END) {
+ mEvents.get(i).mNode.mAnimation.skipToEndValue(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Internal only.
+ *
+ * This method sets the animation values based on the play time. It also fast forward or
+ * backward all the child animations progress accordingly.
+ *
+ * This method is also responsible for calling
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)},
+ * as needed, based on the last play time and current play time.
+ */
+ @Override
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {
+ if (currentPlayTime < 0 || lastPlayTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should never be negative.");
+ }
+ // TODO: take into account repeat counts and repeat callback when repeat is implemented.
+ // Clamp currentPlayTime and lastPlayTime
+
+ // TODO: Make this more efficient
+
+ // Convert the play times to the forward direction.
+ if (inReverse) {
+ if (getTotalDuration() == DURATION_INFINITE) {
+ throw new UnsupportedOperationException("Cannot reverse AnimatorSet with infinite"
+ + " duration");
+ }
+ long duration = getTotalDuration() - mStartDelay;
+ currentPlayTime = Math.min(currentPlayTime, duration);
+ currentPlayTime = duration - currentPlayTime;
+ lastPlayTime = duration - lastPlayTime;
+ inReverse = false;
+ }
+ // Skip all values to start, and iterate mEvents to get animations to the right fraction.
+ skipToStartValue(false);
+
+ ArrayList<Node> unfinishedNodes = new ArrayList<>();
+ // Assumes forward playing from here on.
+ for (int i = 0; i < mEvents.size(); i++) {
+ AnimationEvent event = mEvents.get(i);
+ if (event.getTime() > currentPlayTime) {
+ break;
+ }
+
+ // This animation started prior to the current play time, and won't finish before the
+ // play time, add to the unfinished list.
+ if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ if (event.mNode.mEndTime == DURATION_INFINITE
+ || event.mNode.mEndTime > currentPlayTime) {
+ unfinishedNodes.add(event.mNode);
+ }
+ }
+ // For animations that do finish before the play time, end them in the sequence that
+ // they would in a normal run.
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ // Skip to the end of the animation.
+ event.mNode.mAnimation.skipToEndValue(false);
+ }
+ }
+
+ // Seek unfinished animation to the right time.
+ for (int i = 0; i < unfinishedNodes.size(); i++) {
+ Node node = unfinishedNodes.get(i);
+ long playTime = getPlayTimeForNode(currentPlayTime, node, inReverse);
+ node.mAnimation.animateBasedOnPlayTime(playTime, lastPlayTime, inReverse);
+ }
+ }
+
+ @Override
+ boolean isInitialized() {
+ if (mChildrenInitialized) {
+ return true;
+ }
+
+ boolean allInitialized = true;
+ for (int i = 0; i < mNodes.size(); i++) {
+ if (!mNodes.get(i).mAnimation.isInitialized()) {
+ allInitialized = false;
+ break;
+ }
+ }
+ mChildrenInitialized = allInitialized;
+ return mChildrenInitialized;
+ }
+
+ private void skipToStartValue(boolean inReverse) {
+ skipToEndValue(!inReverse);
+ }
+
+ /**
+ * Sets the position of the animation to the specified point in time. This time should
+ * be between 0 and the total duration of the animation, including any repetition. If
+ * the animation has not yet been started, then it will not advance forward after it is
+ * set to this time; it will simply set the time to this value and perform any appropriate
+ * actions based on that time. If the animation is already running, then setCurrentPlayTime()
+ * will set the current playing time to this value and continue playing from that point.
+ *
+ * @param playTime The time, in milliseconds, to which the animation is advanced or rewound.
+ * Unless the animation is reversing, the playtime is considered the time since
+ * the end of the start delay of the AnimatorSet in a forward playing direction.
+ *
+ */
+ public void setCurrentPlayTime(long playTime) {
+ if (mReversing && getTotalDuration() == DURATION_INFINITE) {
+ // Should never get here
+ throw new UnsupportedOperationException("Error: Cannot seek in reverse in an infinite"
+ + " AnimatorSet");
+ }
+
+ if ((getTotalDuration() != DURATION_INFINITE && playTime > getTotalDuration() - mStartDelay)
+ || playTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should always be in between"
+ + "0 and duration.");
+ }
+
+ initAnimation();
+
+ if (!isStarted()) {
+ if (mReversing) {
+ throw new UnsupportedOperationException("Error: Something went wrong. mReversing"
+ + " should not be set when AnimatorSet is not started.");
+ }
+ if (!mSeekState.isActive()) {
+ findLatestEventIdForTime(0);
+ // Set all the values to start values.
+ initChildren();
+ skipToStartValue(mReversing);
+ mSeekState.setPlayTime(0, mReversing);
+ }
+ animateBasedOnPlayTime(playTime, 0, mReversing);
+ mSeekState.setPlayTime(playTime, mReversing);
+ } else {
+ // If the animation is running, just set the seek time and wait until the next frame
+ // (i.e. doAnimationFrame(...)) to advance the animation.
+ mSeekState.setPlayTime(playTime, mReversing);
+ }
+ }
+
+ private void initChildren() {
+ if (!isInitialized()) {
+ mChildrenInitialized = true;
+ // Forcefully initialize all children based on their end time, so that if the start
+ // value of a child is dependent on a previous animation, the animation will be
+ // initialized after the the previous animations have been advanced to the end.
+ skipToEndValue(false);
+ }
+ }
+
+ /**
+ * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
+ * base.
+ * @return
+ * @hide
+ */
+ @Override
+ public boolean doAnimationFrame(long frameTime) {
+ if (mLastFrameTime < 0) {
+ mFirstFrame = mLastFrameTime = frameTime;
+ }
+
+ // Handle pause/resume
+ if (mPaused) {
+ // Note: Child animations don't receive pause events. Since it's never a contract that
+ // the child animators will be paused when set is paused, this is unlikely to be an
+ // issue.
+ mPauseTime = frameTime;
+ removeAnimationCallback();
+ return false;
+ } else if (mPauseTime > 0) {
+ // Offset by the duration that the animation was paused
+ mFirstFrame += (frameTime - mPauseTime);
+ mPauseTime = -1;
+ }
+
+ // Continue at seeked position
+ if (mSeekState.isActive()) {
+ mSeekState.updateSeekDirection(mReversing);
+ mFirstFrame = frameTime - mSeekState.getPlayTime() - mStartDelay;
+ mSeekState.reset();
+ }
+
+ // This playTime includes the start delay.
+ long playTime = frameTime - mFirstFrame;
+
+ // 1. Pulse the animators that will start or end in this frame
+ // 2. Pulse the animators that will finish in a later frame
+ int latestId = findLatestEventIdForTime(playTime);
+ int startId = mLastEventId;
+
+ handleAnimationEvents(startId, latestId, playTime);
+
+ mLastEventId = latestId;
+
+ // Pump a frame to the on-going animators
+ for (int i = 0; i < mPlayingSet.size(); i++) {
+ Node node = mPlayingSet.get(i);
+ if (!node.mEnded) {
+ node.mEnded = node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node));
+ }
+ }
+
+ // Remove all the finished anims
+ for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
+ if (mPlayingSet.get(i).mEnded) {
+ mPlayingSet.remove(i);
+ }
+ }
+
+ mLastFrameTime = frameTime;
+ if (mPlayingSet.isEmpty()) {
+ boolean finished;
+ if (mReversing) {
+ // Make sure there's no more END event before current event id and after start delay
+ finished = mLastEventId <= 3;
+ } else {
+ // Make sure there's no more START event before current event id:
+ finished = (mLastEventId == mEvents.size() - 1);
+ }
+ if (finished) {
+ endAnimation();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * When playing forward, we call start() at the animation's scheduled start time, and make sure
+ * to pump a frame at the animation's scheduled end time.
+ *
+ * When playing in reverse, we should reverse the animation when we hit animation's end event,
+ * and expect the animation to end at the its delay ended event, rather than start event.
+ */
+ private void handleAnimationEvents(int startId, int latestId, long playTime) {
+ if (mReversing) {
+ startId = startId == -1 ? mEvents.size() : startId;
+ for (int i = startId - 1; i >= latestId; i--) {
+ AnimationEvent event = mEvents.get(i);
+ Node node = event.mNode;
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ mPlayingSet.add(event.mNode);
+ node.mAnimation.startWithoutPulsing(true);
+ node.mAnimation.doAnimationFrame(0);
+ } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED && !node.mEnded) {
+ // end event:
+ node.mEnded =
+ node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node));
+ }
+ }
+ } else {
+ for (int i = startId + 1; i <= latestId; i++) {
+ AnimationEvent event = mEvents.get(i);
+ Node node = event.mNode;
+ if (event.mEvent == AnimationEvent.ANIMATION_START) {
+ mPlayingSet.add(event.mNode);
+ node.mAnimation.startWithoutPulsing(false);
+ node.mAnimation.doAnimationFrame(0);
+ } else if (event.mEvent == AnimationEvent.ANIMATION_END && !node.mEnded) {
+ // start event:
+ node.mEnded =
+ node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node));
+ }
+ }
+ }
+ }
+
+ private long getPlayTimeForNode(long overallPlayTime, Node node) {
+ return getPlayTimeForNode(overallPlayTime, node, mReversing);
+ }
+
+ private long getPlayTimeForNode(long overallPlayTime, Node node, boolean inReverse) {
+ if (inReverse) {
+ overallPlayTime = getTotalDuration() - overallPlayTime;
+ return node.mEndTime - overallPlayTime;
+ } else {
+ return overallPlayTime - node.mStartTime;
+ }
+ }
+
+ private void startAnimation() {
+ // Register animation callback
+ addAnimationCallback(mStartDelay);
+
+ if (mSeekState.getPlayTimeNormalized() == 0 && mReversing) {
+ // Maintain old behavior, if seeked to 0 then call reverse, we'll treat the case
+ // the same as no seeking at all.
+ mSeekState.reset();
+ }
+ // Set the child animators to the right end:
+ if (mShouldResetValuesAtStart) {
+ if (mReversing || isInitialized()) {
+ skipToEndValue(!mReversing);
+ } else {
+ // If not all children are initialized and play direction is forward
+ for (int i = mEvents.size() - 1; i >= 0; i--) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ Animator anim = mEvents.get(i).mNode.mAnimation;
+ // Only reset the animations that have been initialized to start value,
+ // so that if they are defined without a start value, they will get the
+ // values set at the right time (i.e. the next animation run)
+ if (anim.isInitialized()) {
+ anim.skipToEndValue(true);
+ }
+ }
+ }
+ }
+ }
+
+ if (mReversing || mStartDelay == 0 || mSeekState.isActive()) {
+ long playTime;
+ // If no delay, we need to call start on the first animations to be consistent with old
+ // behavior.
+ if (mSeekState.isActive()) {
+ mSeekState.updateSeekDirection(mReversing);
+ playTime = mSeekState.getPlayTime();
+ } else {
+ playTime = 0;
+ }
+ int toId = findLatestEventIdForTime(playTime);
+ handleAnimationEvents(-1, toId, playTime);
+ mLastEventId = toId;
+ }
+ }
+
+ private int findLatestEventIdForTime(long currentPlayTime) {
+ int size = mEvents.size();
+ int latestId = mLastEventId;
+ // Call start on the first animations now to be consistent with the old behavior
+ if (mReversing) {
+ currentPlayTime = getTotalDuration() - currentPlayTime;
+ mLastEventId = mLastEventId == -1 ? size : mLastEventId;
+ for (int j = mLastEventId - 1; j >= 0; j--) {
+ AnimationEvent event = mEvents.get(j);
+ if (event.getTime() >= currentPlayTime) {
+ latestId = j;
+ }
+ }
+ } else {
+ for (int i = mLastEventId + 1; i < size; i++) {
+ AnimationEvent event = mEvents.get(i);
+ if (event.getTime() <= currentPlayTime) {
+ latestId = i;
+ }
+ }
+ }
+ return latestId;
+ }
+
+ private void endAnimation() {
+ mStarted = false;
+ mLastFrameTime = -1;
+ mFirstFrame = -1;
+ mLastEventId = -1;
+ mPaused = false;
+ mPauseTime = -1;
+ mSeekState.reset();
+ mPlayingSet.clear();
+
+ // No longer receive callbacks
+ removeAnimationCallback();
+ // Call end listener
+ if (mListeners != null) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationEnd(this, mReversing);
+ }
+ }
+ mSelfPulse = true;
+ mReversing = false;
+ }
+
+ private void removeAnimationCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.removeCallback(this);
+ }
+
+ private void addAnimationCallback(long delay) {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.addAnimationFrameCallback(this, delay);
}
@Override
@@ -709,13 +1139,20 @@ public final class AnimatorSet extends Animator {
* and will populate any appropriate lists, when it is started.
*/
final int nodeCount = mNodes.size();
- anim.mTerminated = false;
anim.mStarted = false;
- anim.mPlayingSet = new ArrayList<Animator>();
+ anim.mLastFrameTime = -1;
+ anim.mFirstFrame = -1;
+ anim.mLastEventId = -1;
+ anim.mPaused = false;
+ anim.mPauseTime = -1;
+ anim.mSeekState = new SeekState();
+ anim.mSelfPulse = true;
+ anim.mPlayingSet = new ArrayList<Node>();
anim.mNodeMap = new ArrayMap<Animator, Node>();
anim.mNodes = new ArrayList<Node>(nodeCount);
- anim.mReversible = mReversible;
- anim.mSetListener = new AnimatorSetListener(anim);
+ anim.mEvents = new ArrayList<AnimationEvent>();
+ anim.mReversing = false;
+ anim.mDependencyDirty = true;
// Walk through the old nodes list, cloning each node and adding it to the new nodemap.
// One problem is that the old node dependencies point to nodes in the old AnimatorSet.
@@ -727,17 +1164,6 @@ public final class AnimatorSet extends Animator {
node.mTmpClone = nodeClone;
anim.mNodes.add(nodeClone);
anim.mNodeMap.put(nodeClone.mAnimation, nodeClone);
-
- // clear out any listeners that were set up by the AnimatorSet
- final ArrayList<AnimatorListener> cloneListeners = nodeClone.mAnimation.getListeners();
- if (cloneListeners != null) {
- for (int i = cloneListeners.size() - 1; i >= 0; i--) {
- final AnimatorListener listener = cloneListeners.get(i);
- if (listener instanceof AnimatorSetListener) {
- cloneListeners.remove(i);
- }
- }
- }
}
anim.mRootNode = mRootNode.mTmpClone;
@@ -771,89 +1197,6 @@ public final class AnimatorSet extends Animator {
}
- private static class AnimatorSetListener implements AnimatorListener {
-
- private AnimatorSet mAnimatorSet;
-
- AnimatorSetListener(AnimatorSet animatorSet) {
- mAnimatorSet = animatorSet;
- }
-
- public void onAnimationCancel(Animator animation) {
-
- if (!mAnimatorSet.mTerminated) {
- // Listeners are already notified of the AnimatorSet canceling in cancel().
- // The logic below only kicks in when animations end normally
- if (mAnimatorSet.mPlayingSet.size() == 0) {
- ArrayList<AnimatorListener> listeners = mAnimatorSet.mListeners;
- if (listeners != null) {
- int numListeners = listeners.size();
- for (int i = 0; i < numListeners; ++i) {
- listeners.get(i).onAnimationCancel(mAnimatorSet);
- }
- }
- }
- }
- }
-
- @SuppressWarnings("unchecked")
- public void onAnimationEnd(Animator animation) {
- animation.removeListener(this);
- mAnimatorSet.mPlayingSet.remove(animation);
- mAnimatorSet.onChildAnimatorEnded(animation);
- }
-
- // Nothing to do
- public void onAnimationRepeat(Animator animation) {
- }
-
- // Nothing to do
- public void onAnimationStart(Animator animation) {
- }
-
- }
-
- private void onChildAnimatorEnded(Animator animation) {
- Node animNode = mNodeMap.get(animation);
- animNode.mEnded = true;
-
- if (!mTerminated) {
- List<Node> children = animNode.mChildNodes;
- // Start children animations, if any.
- int childrenSize = children == null ? 0 : children.size();
- for (int i = 0; i < childrenSize; i++) {
- if (children.get(i).mLatestParent == animNode) {
- start(children.get(i));
- }
- }
- // Listeners are already notified of the AnimatorSet ending in cancel() or
- // end(); the logic below only kicks in when animations end normally
- boolean allDone = true;
- // Traverse the tree and find if there's any unfinished node
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- if (!mNodes.get(i).mEnded) {
- allDone = false;
- break;
- }
- }
- if (allDone) {
- mStarted = false;
- mPaused = false;
- // If this was the last child animation to end, then notify listeners that this
- // AnimatorSet has ended
- if (mListeners != null) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationEnd(this);
- }
- }
- }
- }
- }
-
/**
* AnimatorSet is only reversible when the set contains no sequential animation, and no child
* animators have a start delay.
@@ -861,32 +1204,21 @@ public final class AnimatorSet extends Animator {
*/
@Override
public boolean canReverse() {
- if (!mReversible) {
- return false;
- }
- // Loop to make sure all the Nodes can reverse.
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- if (!node.mAnimation.canReverse() || node.mAnimation.getStartDelay() > 0) {
- return false;
- }
- }
- return true;
+ return getTotalDuration() != DURATION_INFINITE;
}
/**
- * @hide
+ * Plays the AnimatorSet in reverse. If the animation has been seeked to a specific play time
+ * using {@link #setCurrentPlayTime(long)}, it will play backwards from the point seeked when
+ * reverse was called. Otherwise, then it will start from the end and play backwards. This
+ * behavior is only set for the current animation; future playing of the animation will use the
+ * default behavior of playing forward.
+ * <p>
+ * Note: reverse is not supported for infinite AnimatorSet.
*/
@Override
public void reverse() {
- if (canReverse()) {
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- node.mAnimation.reverse();
- }
- }
+ start(true, true);
}
@Override
@@ -993,18 +1325,61 @@ public final class AnimatorSet extends Animator {
mRootNode.mEndTime = mDelayAnim.getDuration();
updatePlayTime(mRootNode, visited);
- long maxEndTime = 0;
- for (int i = 0; i < size; i++) {
+ sortAnimationEvents();
+ mTotalDuration = mEvents.get(mEvents.size() - 1).getTime();
+ }
+
+ private void sortAnimationEvents() {
+ // Sort the list of events in ascending order of their time
+ // Create the list including the delay animation.
+ mEvents.clear();
+ for (int i = 0; i < mNodes.size(); i++) {
Node node = mNodes.get(i);
- node.mTotalDuration = node.mAnimation.getTotalDuration();
- if (node.mEndTime == DURATION_INFINITE) {
- maxEndTime = DURATION_INFINITE;
- break;
- } else {
- maxEndTime = node.mEndTime > maxEndTime ? node.mEndTime : maxEndTime;
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_START));
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_DELAY_ENDED));
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_END));
+ }
+ mEvents.sort(new Comparator<AnimationEvent>() {
+ @Override
+ public int compare(AnimationEvent e1, AnimationEvent e2) {
+ long t1 = e1.getTime();
+ long t2 = e2.getTime();
+ if (t1 == t2) {
+ if (e1.mNode == e2.mNode) {
+ // For the same animation, start event has to happen before end.
+ return e1.mEvent - e2.mEvent;
+ }
+ // For different animation, end events need to happen before start, to ensure
+ // sequential animations finish the previous one before starting the next one.
+ return e2.mEvent - e1.mEvent;
+ }
+ if (t2 == DURATION_INFINITE) {
+ return -1;
+ }
+ if (t1 == DURATION_INFINITE) {
+ return 1;
+ }
+ // When neither event happens at INFINITE time:
+ return (int) (t1 - t2);
}
+ });
+
+ if (mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_START
+ || mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ throw new UnsupportedOperationException(
+ "Something went wrong, the last event is not an end event");
+ }
+ if (mEvents.get(1).mEvent != AnimationEvent.ANIMATION_DELAY_ENDED
+ || mEvents.get(1).mNode != mRootNode) {
+ throw new UnsupportedOperationException(
+ "Sorting went bad, the root node's start delay end event should always be at"
+ + " index 1");
+ }
+ if (mEvents.get(2).mEvent != AnimationEvent.ANIMATION_END
+ || mEvents.get(2).mNode != mRootNode) {
+ throw new UnsupportedOperationException(
+ "Sorting went bad, the start delay end event should always be at index 2");
}
- mTotalDuration = maxEndTime;
}
/**
@@ -1235,6 +1610,95 @@ public final class AnimatorSet extends Animator {
}
/**
+ * This class is a wrapper around a node and an event for the animation corresponding to the
+ * node. The 3 types of events represent the start of an animation, the end of a start delay of
+ * an animation, and the end of an animation. When playing forward (i.e. in the non-reverse
+ * direction), start event marks when start() should be called, and end event corresponds to
+ * when the animation should finish. When playing in reverse, start delay will not be a part
+ * of the animation. Therefore, reverse() is called at the end event, and animation should end
+ * at the delay ended event.
+ */
+ private static class AnimationEvent {
+ static final int ANIMATION_START = 0;
+ static final int ANIMATION_DELAY_ENDED = 1;
+ static final int ANIMATION_END = 2;
+ final Node mNode;
+ final int mEvent;
+
+ AnimationEvent(Node node, int event) {
+ mNode = node;
+ mEvent = event;
+ }
+
+ long getTime() {
+ if (mEvent == ANIMATION_START) {
+ return mNode.mStartTime;
+ } else if (mEvent == ANIMATION_DELAY_ENDED) {
+ return mNode.mStartTime + mNode.mAnimation.getStartDelay();
+ } else {
+ return mNode.mEndTime;
+ }
+ }
+
+ public String toString() {
+ String eventStr = mEvent == ANIMATION_START ? "start" : (
+ mEvent == ANIMATION_DELAY_ENDED ? "delay ended" : "end");
+ return eventStr + " " + mNode.mAnimation.toString();
+ }
+ }
+
+ private class SeekState {
+ private long mPlayTime = -1;
+ private boolean mSeekingInReverse = false;
+ void reset() {
+ mPlayTime = -1;
+ mSeekingInReverse = false;
+ }
+
+ void setPlayTime(long playTime, boolean inReverse) {
+ // TODO: This can be simplified.
+
+ // Clamp the play time
+ if (getTotalDuration() != DURATION_INFINITE) {
+ mPlayTime = Math.min(playTime, getTotalDuration() - mStartDelay);
+ }
+ mPlayTime = Math.max(0, mPlayTime);
+ mSeekingInReverse = inReverse;
+ }
+
+ void updateSeekDirection(boolean inReverse) {
+ // Change seek direction without changing the overall fraction
+ if (inReverse && getTotalDuration() == DURATION_INFINITE) {
+ throw new UnsupportedOperationException("Error: Cannot reverse infinite animator"
+ + " set");
+ }
+ if (mPlayTime >= 0) {
+ if (inReverse != mSeekingInReverse) {
+ mPlayTime = getTotalDuration() - mStartDelay - mPlayTime;
+ }
+ }
+ }
+
+ long getPlayTime() {
+ return mPlayTime;
+ }
+
+ /**
+ * Returns the playtime assuming the animation is forward playing
+ */
+ long getPlayTimeNormalized() {
+ if (mReversing) {
+ return getTotalDuration() - mStartDelay - mPlayTime;
+ }
+ return mPlayTime;
+ }
+
+ boolean isActive() {
+ return mPlayTime != -1;
+ }
+ }
+
+ /**
* The <code>Builder</code> object is a utility class to facilitate adding animations to a
* <code>AnimatorSet</code> along with the relationships between the various animations. The
* intention of the <code>Builder</code> methods, along with the {@link
@@ -1328,7 +1792,6 @@ public final class AnimatorSet extends Animator {
* {@link AnimatorSet#play(Animator)} method ends.
*/
public Builder before(Animator anim) {
- mReversible = false;
Node node = getNodeForAnimation(anim);
mCurrentNode.addChild(node);
return this;
@@ -1343,7 +1806,6 @@ public final class AnimatorSet extends Animator {
* {@link AnimatorSet#play(Animator)} method to play.
*/
public Builder after(Animator anim) {
- mReversible = false;
Node node = getNodeForAnimation(anim);
mCurrentNode.addParent(node);
return this;
diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java
index 4707bed3ee14..1e1f1554d3a2 100644
--- a/core/java/android/animation/ObjectAnimator.java
+++ b/core/java/android/animation/ObjectAnimator.java
@@ -992,6 +992,11 @@ public final class ObjectAnimator extends ValueAnimator {
}
@Override
+ boolean isInitialized() {
+ return mInitialized;
+ }
+
+ @Override
public ObjectAnimator clone() {
final ObjectAnimator anim = (ObjectAnimator) super.clone();
return anim;
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index f0fc8af9d504..470523fc4252 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -24,6 +24,7 @@ import android.os.Trace;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
@@ -67,7 +68,7 @@ import java.util.HashMap;
* </div>
*/
@SuppressWarnings("unchecked")
-public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
+public class ValueAnimator extends Animator {
private static final String TAG = "ValueAnimator";
private static final boolean DEBUG = false;
@@ -90,7 +91,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
*
* Whenever mStartTime is set, you must also update mStartTimeCommitted.
*/
- long mStartTime;
+ long mStartTime = -1;
/**
* When true, the start time has been firmly committed as a chosen reference point in
@@ -152,7 +153,13 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
/**
* Tracks the time (in milliseconds) when the last frame arrived.
*/
- private long mLastFrameTime = 0;
+ private long mLastFrameTime = -1;
+
+ /**
+ * Tracks the time (in milliseconds) when the first frame arrived. Note the frame may arrive
+ * during the start delay.
+ */
+ private long mFirstFrameTime = -1;
/**
* Additional playing state to indicate whether an animator has been start()'d. There is
@@ -212,6 +219,12 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
private int mRepeatMode = RESTART;
/**
+ * Whether or not the animator should register for its own animation callback to receive
+ * animation pulse.
+ */
+ private boolean mSelfPulse = true;
+
+ /**
* The time interpolator to be used. The elapsed fraction of the animation will be passed
* through this interpolator to calculate the interpolated fraction, which is then used to
* calculate the animated values.
@@ -628,7 +641,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
mSeekFraction = fraction;
}
mOverallFraction = fraction;
- final float currentIterationFraction = getCurrentIterationFraction(fraction);
+ final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
animateValue(currentIterationFraction);
}
@@ -654,11 +667,11 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
* should be played backwards. E.g. When the animation is played backwards in an iteration,
* the fraction for that iteration will go from 1f to 0f.
*/
- private float getCurrentIterationFraction(float fraction) {
+ private float getCurrentIterationFraction(float fraction, boolean inReverse) {
fraction = clampFraction(fraction);
int iteration = getCurrentIteration(fraction);
float currentFraction = fraction - iteration;
- return shouldPlayBackward(iteration) ? 1f - currentFraction : currentFraction;
+ return shouldPlayBackward(iteration, inReverse) ? 1f - currentFraction : currentFraction;
}
/**
@@ -682,18 +695,18 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
* whether the entire animation is being reversed, 2) repeat mode applied to the current
* iteration.
*/
- private boolean shouldPlayBackward(int iteration) {
+ private boolean shouldPlayBackward(int iteration, boolean inReverse) {
if (iteration > 0 && mRepeatMode == REVERSE &&
(iteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
// if we were seeked to some other iteration in a reversing animator,
// figure out the correct direction to start playing based on the iteration
- if (mReversing) {
+ if (inReverse) {
return (iteration % 2) == 0;
} else {
return (iteration % 2) != 0;
}
} else {
- return mReversing;
+ return inReverse;
}
}
@@ -965,7 +978,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationStart(this);
+ tmpListeners.get(i).onAnimationStart(this, mReversing);
}
}
mStartListenersCalled = true;
@@ -984,11 +997,12 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
*
* @param playBackwards Whether the ValueAnimator should start playing in reverse.
*/
- private void start(boolean playBackwards) {
+ private void start(boolean playBackwards, boolean selfPulse) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
+ mSelfPulse = selfPulse;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
@@ -1006,11 +1020,11 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
- mLastFrameTime = 0;
- AnimationHandler animationHandler = AnimationHandler.getInstance();
- animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));
+ mLastFrameTime = -1;
+ mFirstFrameTime = -1;
+ addAnimationCallback((long) (mStartDelay * sDurationScale));
- if (mStartDelay == 0 || mSeekFraction >= 0) {
+ if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// If there's no start delay, init the animation and notify start listeners right away
// to be consistent with the previous behavior. Otherwise, postpone this until the first
// frame after the start delay.
@@ -1026,9 +1040,13 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
}
}
+ void startWithoutPulsing(boolean inReverse) {
+ start(inReverse, false);
+ }
+
@Override
public void start() {
- start(false);
+ start(false, true);
}
@Override
@@ -1073,7 +1091,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
} else if (!mInitialized) {
initAnimation();
}
- animateValue(shouldPlayBackward(mRepeatCount) ? 0f : 1f);
+ animateValue(shouldPlayBackward(mRepeatCount, mReversing) ? 0f : 1f);
endAnimation();
}
@@ -1086,8 +1104,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
if (mPaused && !mResumed) {
mResumed = true;
if (mPauseTime > 0) {
- AnimationHandler handler = AnimationHandler.getInstance();
- handler.addAnimationFrameCallback(this, 0);
+ addAnimationCallback(0);
}
}
super.resume();
@@ -1133,7 +1150,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
mReversing = !mReversing;
end();
} else {
- start(true);
+ start(true, true);
}
}
@@ -1153,8 +1170,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
if (mAnimationEndRequested) {
return;
}
- AnimationHandler handler = AnimationHandler.getInstance();
- handler.removeCallback(this);
+ removeAnimationCallback();
mAnimationEndRequested = true;
mPaused = false;
@@ -1166,16 +1182,18 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
mRunning = false;
mStarted = false;
mStartListenersCalled = false;
+ mLastFrameTime = -1;
+ mFirstFrameTime = -1;
mReversing = false;
- mLastFrameTime = 0;
if (notify && mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationEnd(this);
+ tmpListeners.get(i).onAnimationEnd(this, mReversing);
}
}
+ mReversing = false;
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
@@ -1211,7 +1229,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
* is called (or after start delay if any), which may be before the animation loop starts.
*/
private boolean isPulsingInternal() {
- return mLastFrameTime > 0;
+ return mLastFrameTime >= 0;
}
/**
@@ -1276,24 +1294,116 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
done = true;
}
mOverallFraction = clampFraction(fraction);
- float currentIterationFraction = getCurrentIterationFraction(mOverallFraction);
+ float currentIterationFraction = getCurrentIterationFraction(
+ mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}
/**
+ * Internal use only.
+ *
+ * This method does not modify any fields of the animation. It should be called when seeking
+ * in an AnimatorSet. When the last play time and current play time are of different repeat
+ * iterations,
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)}
+ * will be called.
+ */
+ @Override
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {
+ if (currentPlayTime < 0 || lastPlayTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should never be negative.");
+ }
+
+ initAnimation();
+ // Check whether repeat callback is needed only when repeat count is non-zero
+ if (mRepeatCount > 0) {
+ int iteration = (int) (currentPlayTime / mDuration);
+ int lastIteration = (int) (lastPlayTime / mDuration);
+
+ // Clamp iteration to [0, mRepeatCount]
+ iteration = Math.min(iteration, mRepeatCount);
+ lastIteration = Math.min(lastIteration, mRepeatCount);
+
+ if (iteration != lastIteration) {
+ if (mListeners != null) {
+ int numListeners = mListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ mListeners.get(i).onAnimationRepeat(this);
+ }
+ }
+ }
+ }
+
+ if (mRepeatCount != INFINITE && currentPlayTime >= (mRepeatCount + 1) * mDuration) {
+ skipToEndValue(inReverse);
+ } else {
+ // Find the current fraction:
+ float fraction = currentPlayTime / (float) mDuration;
+ fraction = getCurrentIterationFraction(fraction, inReverse);
+ animateValue(fraction);
+ }
+ }
+
+ /**
+ * Internal use only.
+ * Skips the animation value to end/start, depending on whether the play direction is forward
+ * or backward.
+ *
+ * @param inReverse whether the end value is based on a reverse direction. If yes, this is
+ * equivalent to skip to start value in a forward playing direction.
+ */
+ void skipToEndValue(boolean inReverse) {
+ initAnimation();
+ float endFraction = inReverse ? 0f : 1f;
+ if (mRepeatCount % 2 == 1 && mRepeatMode == REVERSE) {
+ // This would end on fraction = 0
+ endFraction = 0f;
+ }
+ animateValue(endFraction);
+ }
+
+ /**
* Processes a frame of the animation, adjusting the start time if needed.
*
* @param frameTime The frame time.
* @return true if the animation has ended.
* @hide
*/
- public final void doAnimationFrame(long frameTime) {
- AnimationHandler handler = AnimationHandler.getInstance();
- if (mLastFrameTime == 0) {
+ public final boolean doAnimationFrame(long frameTime) {
+ if (!mRunning && mStartTime < 0) {
+ // First frame during delay
+ mStartTime = frameTime + mStartDelay;
+ }
+
+ // Handle pause/resume
+ if (mPaused) {
+ mPauseTime = frameTime;
+ removeAnimationCallback();
+ return false;
+ } else if (mResumed) {
+ mResumed = false;
+ if (mPauseTime > 0) {
+ // Offset by the duration that the animation was paused
+ mStartTime += (frameTime - mPauseTime);
+ }
+ }
+
+ if (!mRunning) {
+ // If not running, that means the animation is in the start delay phase. In the case of
+ // reversing, we want to run start delay in the end.
+ if (mStartTime > frameTime) {
+ // During start delay
+ return false;
+ } else {
+ // Start delay has passed.
+ mRunning = true;
+ }
+ }
+
+ if (mLastFrameTime < 0) {
// First frame
- handler.addOneShotCommitCallback(this);
if (mStartDelay > 0) {
startAnimation();
}
@@ -1307,19 +1417,6 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
mLastFrameTime = frameTime;
- if (mPaused) {
- mPauseTime = frameTime;
- handler.removeCallback(this);
- return;
- } else if (mResumed) {
- mResumed = false;
- if (mPauseTime > 0) {
- // Offset by the duration that the animation was paused
- mStartTime += (frameTime - mPauseTime);
- mStartTimeCommitted = false; // allow start time to be compensated for jank
- }
- handler.addOneShotCommitCallback(this);
- }
// The frame time might be before the start time during the first frame of
// an animation. The "current time" must always be on or after the start
// time to avoid animating frames at negative time intervals. In practice, this
@@ -1330,6 +1427,31 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
if (finished) {
endAnimation();
}
+ return finished;
+ }
+
+ private void addOneShotCommitCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.addOneShotCommitCallback(this);
+ }
+
+ private void removeAnimationCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.removeCallback(this);
+ }
+
+ private void addAnimationCallback(long delay) {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.addAnimationFrameCallback(this, delay);
}
/**
@@ -1384,11 +1506,12 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
anim.mPaused = false;
anim.mResumed = false;
anim.mStartListenersCalled = false;
- anim.mStartTime = 0;
+ anim.mStartTime = -1;
anim.mStartTimeCommitted = false;
anim.mAnimationEndRequested = false;
- anim.mPauseTime = 0;
- anim.mLastFrameTime = 0;
+ anim.mPauseTime = -1;
+ anim.mLastFrameTime = -1;
+ anim.mFirstFrameTime = -1;
anim.mOverallFraction = 0;
anim.mCurrentFraction = 0;