pause/resume for Animators

It is now possible to pause Animator-based animations. Pausing an
animator causes it to hold the current time/value indefinitely, or
until end/cancel/resume is called. When resume() is called, it continues
from where it left off.

There is a new listener interface on Animator, AnimatorPauseListener,
which can be used to listen to pause/resume events.

Change-Id: I77d1535e792fb7bf349f549a0ac0a0d85958cb47
This commit is contained in:
Chet Haase
2013-08-08 14:00:00 -07:00
parent 9a68f82f37
commit 8aa1ffb0ed
7 changed files with 430 additions and 11 deletions

View File

@@ -2338,17 +2338,23 @@ package android.animation {
public abstract class Animator implements java.lang.Cloneable {
ctor public Animator();
method public void addListener(android.animation.Animator.AnimatorListener);
method public void addPauseListener(android.animation.Animator.AnimatorPauseListener);
method public void cancel();
method public android.animation.Animator clone();
method public void end();
method public abstract long getDuration();
method public android.animation.TimeInterpolator getInterpolator();
method public java.util.ArrayList<android.animation.Animator.AnimatorListener> getListeners();
method public java.util.ArrayList<android.animation.Animator.AnimatorPauseListener> getPauseListeners();
method public abstract long getStartDelay();
method public boolean isPaused();
method public abstract boolean isRunning();
method public boolean isStarted();
method public void pause();
method public void removeAllListeners();
method public void removeListener(android.animation.Animator.AnimatorListener);
method public void removePauseListener(android.animation.Animator.AnimatorPauseListener);
method public void resume();
method public abstract android.animation.Animator setDuration(long);
method public abstract void setInterpolator(android.animation.TimeInterpolator);
method public abstract void setStartDelay(long);
@@ -2365,16 +2371,23 @@ package android.animation {
method public abstract void onAnimationStart(android.animation.Animator);
}
public static abstract interface Animator.AnimatorPauseListener {
method public abstract void onAnimationPause(android.animation.Animator);
method public abstract void onAnimationResume(android.animation.Animator);
}
public class AnimatorInflater {
ctor public AnimatorInflater();
method public static android.animation.Animator loadAnimator(android.content.Context, int) throws android.content.res.Resources.NotFoundException;
}
public abstract class AnimatorListenerAdapter implements android.animation.Animator.AnimatorListener {
public abstract class AnimatorListenerAdapter implements android.animation.Animator.AnimatorListener android.animation.Animator.AnimatorPauseListener {
ctor public AnimatorListenerAdapter();
method public void onAnimationCancel(android.animation.Animator);
method public void onAnimationEnd(android.animation.Animator);
method public void onAnimationPause(android.animation.Animator);
method public void onAnimationRepeat(android.animation.Animator);
method public void onAnimationResume(android.animation.Animator);
method public void onAnimationStart(android.animation.Animator);
}

View File

@@ -29,6 +29,17 @@ public abstract class Animator implements Cloneable {
*/
ArrayList<AnimatorListener> mListeners = null;
/**
* The set of listeners to be sent pause/resume events through the life
* of an animation.
*/
ArrayList<AnimatorPauseListener> mPauseListeners = null;
/**
* Whether this animator is currently in a paused state.
*/
boolean mPaused = false;
/**
* Starts this animation. If the animation has a nonzero startDelay, the animation will start
* running after that delay elapses. A non-delayed animation will have its initial
@@ -68,6 +79,66 @@ public abstract class Animator implements Cloneable {
public void end() {
}
/**
* Pauses a running animation. This method should only be called on the same thread on
* which the animation was started. If the animation has not yet been {@link
* #isStarted() started} or has since ended, then the call is ignored. Paused
* animations can be resumed by calling {@link #resume()}.
*
* @see #resume()
* @see #isPaused()
* @see AnimatorPauseListener
*/
public void pause() {
if (isStarted() && !mPaused) {
mPaused = true;
if (mPauseListeners != null) {
ArrayList<AnimatorPauseListener> tmpListeners =
(ArrayList<AnimatorPauseListener>) mPauseListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationPause(this);
}
}
}
}
/**
* Resumes a paused animation, causing the animator to pick up where it left off
* when it was paused. This method should only be called on the same thread on
* which the animation was started. Calls to resume() on an animator that is
* not currently paused will be ignored.
*
* @see #pause()
* @see #isPaused()
* @see AnimatorPauseListener
*/
public void resume() {
if (mPaused) {
mPaused = false;
if (mPauseListeners != null) {
ArrayList<AnimatorPauseListener> tmpListeners =
(ArrayList<AnimatorPauseListener>) mPauseListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationResume(this);
}
}
}
}
/**
* Returns whether this animator is currently in a paused state.
*
* @return True if the animator is currently paused, false otherwise.
*
* @see #pause()
* @see #resume()
*/
public boolean isPaused() {
return mPaused;
}
/**
* The amount of time, in milliseconds, to delay processing the animation
* after {@link #start()} is called.
@@ -179,16 +250,59 @@ public abstract class Animator implements Cloneable {
return mListeners;
}
/**
* Adds a pause listener to this animator.
*
* @param listener the listener to be added to the current set of pause listeners
* for this animation.
*/
public void addPauseListener(AnimatorPauseListener listener) {
if (mPauseListeners == null) {
mPauseListeners = new ArrayList<AnimatorPauseListener>();
}
mPauseListeners.add(listener);
}
/**
* Removes a pause listener from the set listening to this animation.
*
* @param listener the listener to be removed from the current set of pause
* listeners for this animation.
*/
public void removePauseListener(AnimatorPauseListener listener) {
if (mPauseListeners == null) {
return;
}
mPauseListeners.remove(listener);
if (mPauseListeners.size() == 0) {
mPauseListeners = null;
}
}
/**
* Gets the set of {@link AnimatorPauseListener} objects that are currently
* listening for pause/resume events on this animator.
*
* @return ArrayList<AnimatorListener> The set of pause listeners.
*/
public ArrayList<AnimatorPauseListener> getPauseListeners() {
return mPauseListeners;
}
/**
* Removes all listeners from this object. This is equivalent to calling
* <code>getListeners()</code> followed by calling <code>clear()</code> on the
* returned list of listeners.
* {@link #getListeners()} and {@link #getPauseListeners()} followed by calling
* {@link ArrayList#clear()} on the returned lists of listeners.
*/
public void removeAllListeners() {
if (mListeners != null) {
mListeners.clear();
mListeners = null;
}
if (mPauseListeners != null) {
mPauseListeners.clear();
mPauseListeners = null;
}
}
@Override
@@ -203,6 +317,14 @@ public abstract class Animator implements Cloneable {
anim.mListeners.add(oldListeners.get(i));
}
}
if (mPauseListeners != null) {
ArrayList<AnimatorPauseListener> oldListeners = mPauseListeners;
anim.mPauseListeners = new ArrayList<AnimatorPauseListener>();
int numListeners = oldListeners.size();
for (int i = 0; i < numListeners; ++i) {
anim.mPauseListeners.add(oldListeners.get(i));
}
}
return anim;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
@@ -280,4 +402,29 @@ public abstract class Animator implements Cloneable {
*/
void onAnimationRepeat(Animator animation);
}
/**
* A pause listener receives notifications from an animation when the
* animation is {@link #pause() paused} or {@link #resume() resumed}.
*
* @see #addPauseListener(AnimatorPauseListener)
*/
public static interface AnimatorPauseListener {
/**
* <p>Notifies that the animation was paused.</p>
*
* @param animation The animaton being paused.
* @see #pause()
*/
void onAnimationPause(Animator animation);
/**
* <p>Notifies that the animation was resumed, after being
* previously paused.</p>
*
* @param animation The animation being resumed.
* @see #resume()
*/
void onAnimationResume(Animator animation);
}
}

View File

@@ -21,7 +21,8 @@ package android.animation;
* Any custom listener that cares only about a subset of the methods of this listener can
* simply subclass this adapter class instead of implementing the interface directly.
*/
public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener {
public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener,
Animator.AnimatorPauseListener {
/**
* {@inheritDoc}
@@ -51,4 +52,17 @@ public abstract class AnimatorListenerAdapter implements Animator.AnimatorListen
public void onAnimationStart(Animator animation) {
}
/**
* {@inheritDoc}
*/
@Override
public void onAnimationPause(Animator animation) {
}
/**
* {@inheritDoc}
*/
@Override
public void onAnimationResume(Animator animation) {
}
}

View File

@@ -455,6 +455,36 @@ public final class AnimatorSet extends Animator {
}
}
@Override
public void pause() {
boolean previouslyPaused = mPaused;
super.pause();
if (!previouslyPaused && mPaused) {
if (mDelayAnim != null) {
mDelayAnim.pause();
} else {
for (Node node : mNodes) {
node.animation.pause();
}
}
}
}
@Override
public void resume() {
boolean previouslyPaused = mPaused;
super.resume();
if (previouslyPaused && !mPaused) {
if (mDelayAnim != null) {
mDelayAnim.resume();
} else {
for (Node node : mNodes) {
node.animation.resume();
}
}
}
}
/**
* {@inheritDoc}
*
@@ -467,6 +497,7 @@ public final class AnimatorSet extends Animator {
public void start() {
mTerminated = false;
mStarted = true;
mPaused = false;
if (mDuration >= 0) {
// If the duration was set on this AnimatorSet, pass it along to all child animations
@@ -549,6 +580,7 @@ public final class AnimatorSet extends Animator {
mPlayingSet.add(node.animation);
}
}
mDelayAnim = null;
}
});
mDelayAnim.start();
@@ -787,6 +819,7 @@ public final class AnimatorSet extends Animator {
}
}
mAnimatorSet.mStarted = false;
mAnimatorSet.mPaused = false;
}
}
}

View File

@@ -80,6 +80,20 @@ public class ValueAnimator extends Animator {
*/
long mSeekTime = -1;
/**
* Set on the next frame after pause() is called, used to calculate a new startTime
* or delayStartTime which allows the animator to continue from the point at which
* it was paused. If negative, has not yet been set.
*/
private long mPauseTime;
/**
* Set when an animator is resumed. This triggers logic in the next frame which
* actually resumes the animator.
*/
private boolean mResumed = false;
// The static sAnimationHandler processes the internal timing loop on which all animations
// are based
/**
@@ -147,7 +161,7 @@ public class ValueAnimator extends Animator {
private boolean mStarted = false;
/**
* Tracks whether we've notified listeners of the onAnimationSTart() event. This can be
* Tracks whether we've notified listeners of the onAnimationStart() event. This can be
* complex to keep track of since we notify listeners at different times depending on
* startDelay and whether start() was called before end().
*/
@@ -914,6 +928,7 @@ public class ValueAnimator extends Animator {
mPlayingState = STOPPED;
mStarted = true;
mStartedDelay = false;
mPaused = false;
AnimationHandler animationHandler = getOrCreateAnimationHandler();
animationHandler.mPendingAnimations.add(this);
if (mStartDelay == 0) {
@@ -970,6 +985,24 @@ public class ValueAnimator extends Animator {
endAnimation(handler);
}
@Override
public void resume() {
if (mPaused) {
mResumed = true;
}
super.resume();
}
@Override
public void pause() {
boolean previouslyPaused = mPaused;
super.pause();
if (!previouslyPaused && mPaused) {
mPauseTime = -1;
mResumed = false;
}
}
@Override
public boolean isRunning() {
return (mPlayingState == RUNNING || mRunning);
@@ -1008,6 +1041,7 @@ public class ValueAnimator extends Animator {
handler.mPendingAnimations.remove(this);
handler.mDelayedAnims.remove(this);
mPlayingState = STOPPED;
mPaused = false;
if ((mStarted || mRunning) && mListeners != null) {
if (!mRunning) {
// If it's not yet running, then start listeners weren't called. Call them now.
@@ -1071,6 +1105,18 @@ public class ValueAnimator extends Animator {
mStartedDelay = true;
mDelayStartTime = currentTime;
} else {
if (mPaused) {
if (mPauseTime < 0) {
mPauseTime = currentTime;
}
return false;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
// Offset by the duration that the animation was paused
mDelayStartTime += (currentTime - mPauseTime);
}
}
long deltaTime = currentTime - mDelayStartTime;
if (deltaTime > mStartDelay) {
// startDelay ended - start the anim and record the
@@ -1093,7 +1139,7 @@ public class ValueAnimator extends Animator {
*
* @param currentTime The current time, as tracked by the static timing handler
* @return true if the animation's duration, including any repetitions due to
* <code>repeatCount</code> has been exceeded and the animation should be ended.
* <code>repeatCount</code>, has been exceeded and the animation should be ended.
*/
boolean animationFrame(long currentTime) {
boolean done = false;
@@ -1148,6 +1194,18 @@ public class ValueAnimator extends Animator {
mSeekTime = -1;
}
}
if (mPaused) {
if (mPauseTime < 0) {
mPauseTime = frameTime;
}
return false;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
// Offset by the duration that the animation was paused
mStartTime += (frameTime - mPauseTime);
}
}
// 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

View File

@@ -37,14 +37,12 @@ public class AnimatorSetEventsTest extends EventsTest {
button = (Button) getActivity().findViewById(R.id.animatingButton);
mAnimator = new AnimatorSet();
((AnimatorSet)mAnimator).playSequentially(xAnim, yAnim);
super.setUp();
}
@Override
protected long getTimeout() {
return (xAnim.getDuration() + yAnim.getDuration()) +
(xAnim.getStartDelay() + yAnim.getStartDelay()) +
return (2 * mAnimator.getDuration()) + (2 * mAnimator.getStartDelay()) +
ANIM_DELAY + FUTURE_RELEASE_DELAY;
}

View File

@@ -22,6 +22,7 @@ import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Tests for the various lifecycle events of Animators. This abstract class is subclassed by
@@ -42,12 +43,15 @@ public abstract class EventsTest
protected static final int ANIM_DELAY = 100;
protected static final int ANIM_MID_DURATION = ANIM_DURATION / 2;
protected static final int ANIM_MID_DELAY = ANIM_DELAY / 2;
protected static final int ANIM_PAUSE_DURATION = ANIM_DELAY;
protected static final int ANIM_PAUSE_DELAY = ANIM_DELAY / 2;
protected static final int FUTURE_RELEASE_DELAY = 50;
protected static final int ANIM_FULL_DURATION_SLOP = 100;
private boolean mStarted; // tracks whether we've received the onAnimationStart() callback
protected boolean mRunning; // tracks whether we've started the animator
private boolean mCanceled; // trackes whether we've canceled the animator
protected Animator.AnimatorListener mFutureListener; // mechanism for delaying the end of the test
private boolean mCanceled; // tracks whether we've canceled the animator
protected Animator.AnimatorListener mFutureListener; // mechanism for delaying end of the test
protected FutureWaiter mFuture; // Mechanism for waiting for the UI test to complete
private Animator.AnimatorListener mListener; // Listener that handles/tests the events
@@ -103,6 +107,48 @@ public abstract class EventsTest
}
};
/**
* Pauses the given animator. Used to delay pausing until some later time (after the
* animator has started playing).
*/
static class Pauser implements Runnable {
Animator mAnim;
FutureWaiter mFuture;
public Pauser(Animator anim, FutureWaiter future) {
mAnim = anim;
mFuture = future;
}
@Override
public void run() {
try {
mAnim.pause();
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
};
/**
* Resumes the given animator. Used to delay resuming until some later time (after the
* animator has paused for some duration).
*/
static class Resumer implements Runnable {
Animator mAnim;
FutureWaiter mFuture;
public Resumer(Animator anim, FutureWaiter future) {
mAnim = anim;
mFuture = future;
}
@Override
public void run() {
try {
mAnim.resume();
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
};
/**
* Releases the given Future object when the listener's end() event is called. Specifically,
* it releases it after some further delay, to give the test time to do other things right
@@ -555,4 +601,114 @@ public abstract class EventsTest
mFuture.get(getTimeout(), TimeUnit.MILLISECONDS);
}
/**
* Verify that pausing and resuming an animator ends within
* the appropriate timeout duration.
*/
@MediumTest
public void testPauseResume() throws Exception {
mFutureListener = new FutureReleaseListener(mFuture);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
Handler handler = new Handler();
mAnimator.addListener(mFutureListener);
mRunning = true;
mAnimator.start();
handler.postDelayed(new Pauser(mAnimator, mFuture), ANIM_PAUSE_DELAY);
handler.postDelayed(new Resumer(mAnimator, mFuture),
ANIM_PAUSE_DELAY + ANIM_PAUSE_DURATION);
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
});
mFuture.get(getTimeout() + ANIM_PAUSE_DURATION, TimeUnit.MILLISECONDS);
}
/**
* Verify that pausing and resuming a startDelayed animator ends within
* the appropriate timeout duration.
*/
@MediumTest
public void testPauseResumeDelayed() throws Exception {
mAnimator.setStartDelay(ANIM_DELAY);
mFutureListener = new FutureReleaseListener(mFuture);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
Handler handler = new Handler();
mAnimator.addListener(mFutureListener);
mRunning = true;
mAnimator.start();
handler.postDelayed(new Pauser(mAnimator, mFuture), ANIM_PAUSE_DELAY);
handler.postDelayed(new Resumer(mAnimator, mFuture),
ANIM_PAUSE_DELAY + ANIM_PAUSE_DURATION);
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
});
mFuture.get(getTimeout() + ANIM_PAUSE_DURATION + ANIM_FULL_DURATION_SLOP,
TimeUnit.MILLISECONDS);
}
/**
* Verify that pausing an animator without resuming it causes a timeout.
*/
@MediumTest
public void testPauseTimeout() throws Exception {
mFutureListener = new FutureReleaseListener(mFuture);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
Handler handler = new Handler();
mAnimator.addListener(mFutureListener);
mRunning = true;
mAnimator.start();
handler.postDelayed(new Pauser(mAnimator, mFuture), ANIM_PAUSE_DELAY);
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
});
try {
mFuture.get(getTimeout() + ANIM_PAUSE_DURATION + ANIM_FULL_DURATION_SLOP,
TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// Expected behavior, swallow the exception
}
}
/**
* Verify that pausing a startDelayed animator without resuming it causes a timeout.
*/
@MediumTest
public void testPauseTimeoutDelayed() throws Exception {
mAnimator.setStartDelay(ANIM_DELAY);
mFutureListener = new FutureReleaseListener(mFuture);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
Handler handler = new Handler();
mAnimator.addListener(mFutureListener);
mRunning = true;
mAnimator.start();
handler.postDelayed(new Pauser(mAnimator, mFuture), ANIM_PAUSE_DELAY);
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
});
try {
mFuture.get(getTimeout() + ANIM_PAUSE_DURATION + ANIM_FULL_DURATION_SLOP,
TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// Expected behavior, swallow the exception
}
}
}