From 8aa1ffb0ed292891030992c65df4e5dc8bd37524 Mon Sep 17 00:00:00 2001 From: Chet Haase Date: Thu, 8 Aug 2013 14:00:00 -0700 Subject: [PATCH] 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 --- api/current.txt | 15 +- core/java/android/animation/Animator.java | 151 ++++++++++++++++- .../animation/AnimatorListenerAdapter.java | 16 +- core/java/android/animation/AnimatorSet.java | 33 ++++ .../java/android/animation/ValueAnimator.java | 62 ++++++- .../animation/AnimatorSetEventsTest.java | 4 +- .../src/android/animation/EventsTest.java | 160 +++++++++++++++++- 7 files changed, 430 insertions(+), 11 deletions(-) diff --git a/api/current.txt b/api/current.txt index 6026cc52d1dba..96289dc4088af 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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 getListeners(); + method public java.util.ArrayList 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); } diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index 39eb8d6abe843..89accbbdc88e1 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -29,6 +29,17 @@ public abstract class Animator implements Cloneable { */ ArrayList mListeners = null; + /** + * The set of listeners to be sent pause/resume events through the life + * of an animation. + */ + ArrayList 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 tmpListeners = + (ArrayList) 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 tmpListeners = + (ArrayList) 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(); + } + 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 The set of pause listeners. + */ + public ArrayList getPauseListeners() { + return mPauseListeners; + } + /** * Removes all listeners from this object. This is equivalent to calling - * getListeners() followed by calling clear() 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 oldListeners = mPauseListeners; + anim.mPauseListeners = new ArrayList(); + 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 { + /** + *

Notifies that the animation was paused.

+ * + * @param animation The animaton being paused. + * @see #pause() + */ + void onAnimationPause(Animator animation); + + /** + *

Notifies that the animation was resumed, after being + * previously paused.

+ * + * @param animation The animation being resumed. + * @see #resume() + */ + void onAnimationResume(Animator animation); + } } diff --git a/core/java/android/animation/AnimatorListenerAdapter.java b/core/java/android/animation/AnimatorListenerAdapter.java index e5d70a4f017d4..2ecb8c3dd4090 100644 --- a/core/java/android/animation/AnimatorListenerAdapter.java +++ b/core/java/android/animation/AnimatorListenerAdapter.java @@ -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) { + } } diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index b48853b4f0f01..018a2d66ee131 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -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; } } } diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index e370e4aff9858..63942996c3e9d 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -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 - * repeatCount has been exceeded and the animation should be ended. + * repeatCount, 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 diff --git a/core/tests/coretests/src/android/animation/AnimatorSetEventsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetEventsTest.java index d415e4ea056cc..7eb32ee36876c 100644 --- a/core/tests/coretests/src/android/animation/AnimatorSetEventsTest.java +++ b/core/tests/coretests/src/android/animation/AnimatorSetEventsTest.java @@ -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; } diff --git a/core/tests/coretests/src/android/animation/EventsTest.java b/core/tests/coretests/src/android/animation/EventsTest.java index 8df711b913564..28cfe3d5d68b8 100644 --- a/core/tests/coretests/src/android/animation/EventsTest.java +++ b/core/tests/coretests/src/android/animation/EventsTest.java @@ -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 + } + } }