diff --git a/api/current.txt b/api/current.txt index 18e4adfc70e6f..fe9d8d08c4578 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); method public void playTogether(android.animation.Animator...); method public void playTogether(java.util.Collection); + 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 a192c9265be3c..b55bd60bd76c5 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); method public void playTogether(android.animation.Animator...); method public void playTogether(java.util.Collection); + 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 47440e6637fbb..950c3d7d29a86 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); method public void playTogether(android.animation.Animator...); method public void playTogether(java.util.Collection); + 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 95262ab46a9e5..e2e5a8f66288c 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 c51725a2caac4..634dc1fd6ee7f 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 AnimatorListeners 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). @@ -464,12 +464,103 @@ public abstract class Animator implements Cloneable { throw new IllegalStateException("Reverse is not supported"); } + /** + * @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) {} + /** *

An animation listener receives notifications from an animation. * Notifications indicate animation related events, such as the end or the * repetition of the animation.

*/ public static interface AnimatorListener { + + /** + *

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

Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + *

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

Notifies the start of the animation.

* diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index a904f9d5447b5..d5814a30ac170 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 developer guide.

* */ -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 mPlayingSet = new ArrayList(); + private ArrayList mPlayingSet = new ArrayList(); /** * Contains all nodes, mapped to their respective Animators. When new @@ -76,6 +80,11 @@ public final class AnimatorSet extends Animator { */ private ArrayMap mNodeMap = new ArrayMap(); + /** + * Contains the start and end events of all the nodes. All these events are sorted in this list. + */ + private ArrayList 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 @@ -83,21 +92,6 @@ public final class AnimatorSet extends Animator { */ private ArrayList mNodes = new ArrayList(); - /** - * 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 tmpListeners = null; if (mListeners != null) { @@ -360,18 +401,13 @@ public final class AnimatorSet extends Animator { tmpListeners.get(i).onAnimationCancel(this); } } - ArrayList playingSet = new ArrayList<>(mPlayingSet); + ArrayList 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 tmpListeners = - (ArrayList) 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 remainingList = new ArrayList(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) 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 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 tmpListeners = + (ArrayList) 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(); + 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(); anim.mNodeMap = new ArrayMap(); anim.mNodes = new ArrayList(nodeCount); - anim.mReversible = mReversible; - anim.mSetListener = new AnimatorSetListener(anim); + anim.mEvents = new ArrayList(); + 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 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 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 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 tmpListeners = - (ArrayList) 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. + *

+ * 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() { + @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; } /** @@ -1234,6 +1609,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 Builder object is a utility class to facilitate adding animations to a * AnimatorSet along with the relationships between the various animations. The @@ -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 4707bed3ee143..1e1f1554d3a28 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -991,6 +991,11 @@ public final class ObjectAnimator extends ValueAnimator { } } + @Override + boolean isInitialized() { + return mInitialized; + } + @Override public ObjectAnimator clone() { final ObjectAnimator anim = (ObjectAnimator) super.clone(); diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index f0fc8af9d504e..470523fc42522 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; * */ @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 @@ -211,6 +218,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 @@ -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) 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 tmpListeners = (ArrayList) 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,12 +1294,76 @@ 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. * @@ -1289,11 +1371,39 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio * @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;