diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index 5fe3644f1297c..57e0583a8b2c5 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -32,10 +32,9 @@ public abstract class Animator implements Cloneable { /** * Starts this animation. If the animation has a nonzero startDelay, the animation will start - * running after that delay elapses. Note that the animation does not start synchronously with - * this call, because all animation events are posted to a central timing loop so that animation - * times are all synchronized on a single timing pulse on the UI thread. So the animation will - * start the next time that event handler processes events. + * running after that delay elapses. A non-delayed animation will have its initial + * value(s) set immediately, followed by calls to + * {@link AnimatorListener#onAnimationStart(Animator)} for any listeners of this animator. * *

The animation started by calling this method will be run on the thread that called * this method. This thread should have a Looper on it (a runtime exception will be thrown if diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 61a12eec6baa9..ce3dd13e4dfb8 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -349,7 +349,8 @@ public final class AnimatorSet extends Animator { return true; } } - return false; + // Also return true if we're currently running the startDelay animator + return (mDelayAnim != null && mDelayAnim.isRunning()); } /** @@ -487,7 +488,6 @@ public final class AnimatorSet extends Animator { mPlayingSet.add(node.animation); } } else { - // TODO: Need to cancel out of the delay appropriately mDelayAnim = ValueAnimator.ofFloat(0f, 1f); mDelayAnim.setDuration(mStartDelay); mDelayAnim.addListener(new AnimatorListenerAdapter() { diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 90d676ea29041..c22306abcdc76 100755 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -185,6 +185,16 @@ public class ValueAnimator extends Animator { */ int mPlayingState = STOPPED; + /** + * Additional playing state to indicate whether an animator has been start()'d. There is + * some lag between a call to start() and the first animation frame. We should still note + * that the animation has been started, even if it's first animation frame has not yet + * happened, and reflect that state in isRunning(). + * Note that delayed animations are different: they are not started until their first + * animation frame, which occurs after their delay elapses. + */ + private boolean mStarted = false; + /** * Flag that denotes whether the animation is set up and ready to go. Used to * set up animation that has not yet been started. @@ -618,6 +628,7 @@ public class ValueAnimator extends Animator { for (int i = 0; i < numReadyAnims; ++i) { ValueAnimator anim = readyAnims.get(i); anim.startAnimation(); + anim.mStarted = true; delayedAnims.remove(anim); } readyAnims.clear(); @@ -908,6 +919,7 @@ public class ValueAnimator extends Animator { // This sets the initial value of the animation, prior to actually starting it running setCurrentPlayTime(getCurrentPlayTime()); mPlayingState = STOPPED; + mStarted = true; if (mListeners != null) { ArrayList tmpListeners = @@ -937,7 +949,8 @@ public class ValueAnimator extends Animator { // to run if (mPlayingState != STOPPED || sPendingAnimations.get().contains(this) || sDelayedAnims.get().contains(this)) { - if (mListeners != null) { + // Only notify listeners if the animator has actually started + if (mStarted && mListeners != null) { ArrayList tmpListeners = (ArrayList) mListeners.clone(); for (AnimatorListener listener : tmpListeners) { @@ -969,7 +982,7 @@ public class ValueAnimator extends Animator { @Override public boolean isRunning() { - return (mPlayingState == RUNNING); + return (mPlayingState == RUNNING || mStarted); } /** @@ -1000,7 +1013,7 @@ public class ValueAnimator extends Animator { sPendingAnimations.get().remove(this); sDelayedAnims.get().remove(this); mPlayingState = STOPPED; - if (mListeners != null) { + if (mStarted && mListeners != null) { ArrayList tmpListeners = (ArrayList) mListeners.clone(); int numListeners = tmpListeners.size(); @@ -1008,6 +1021,7 @@ public class ValueAnimator extends Animator { tmpListeners.get(i).onAnimationEnd(this); } } + mStarted = false; } /** diff --git a/core/tests/coretests/src/android/animation/EventsTest.java b/core/tests/coretests/src/android/animation/EventsTest.java index f970ffc59806d..6ea28452c9618 100644 --- a/core/tests/coretests/src/android/animation/EventsTest.java +++ b/core/tests/coretests/src/android/animation/EventsTest.java @@ -21,6 +21,8 @@ import android.test.UiThreadTest; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; +import java.util.concurrent.TimeUnit; + /** * Tests for the various lifecycle events of Animators. This abstract class is subclassed by * concrete implementations that provide the actual Animator objects being tested. All of the @@ -40,7 +42,10 @@ public abstract class EventsTest private static final int ANIM_DELAY = 100; private static final int ANIM_MID_DURATION = ANIM_DURATION / 2; private static final int ANIM_MID_DELAY = ANIM_DELAY / 2; + private static final int FUTURE_RELEASE_DELAY = 50; + private static final int TIMEOUT = ANIM_DURATION + ANIM_DELAY + FUTURE_RELEASE_DELAY; + private boolean mStarted; // tracks whether we've received the onAnimationStart() callback private boolean mRunning; // tracks whether we've started the animator private boolean mCanceled; // trackes whether we've canceled the animator private Animator.AnimatorListener mFutureListener; // mechanism for delaying the end of the test @@ -51,17 +56,44 @@ public abstract class EventsTest // setup() method prior to calling the superclass setup() /** - * Cancels the given animator. Used to delay cancelation until some later time (after the + * Cancels the given animator. Used to delay cancellation until some later time (after the * animator has started playing). */ static class Canceler implements Runnable { Animator mAnim; - public Canceler(Animator anim) { + FutureWaiter mFuture; + public Canceler(Animator anim, FutureWaiter future) { mAnim = anim; + mFuture = future; } @Override public void run() { - mAnim.cancel(); + try { + mAnim.cancel(); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }; + + /** + * Ends the given animator. Used to delay ending until some later time (after the + * animator has started playing). + */ + static class Ender implements Runnable { + Animator mAnim; + FutureWaiter mFuture; + public Ender(Animator anim, FutureWaiter future) { + mAnim = anim; + mFuture = future; + } + @Override + public void run() { + try { + mAnim.end(); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } } }; @@ -76,6 +108,23 @@ public abstract class EventsTest public FutureReleaseListener(FutureWaiter future) { mFuture = future; } + + /** + * Variant constructor that auto-releases the FutureWaiter after the specified timeout. + * @param future + * @param timeout + */ + public FutureReleaseListener(FutureWaiter future, long timeout) { + mFuture = future; + Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + mFuture.release(); + } + }, timeout); + } + @Override public void onAnimationEnd(Animator animation) { Handler handler = new Handler(); @@ -84,7 +133,7 @@ public abstract class EventsTest public void run() { mFuture.release(); } - }, ANIM_MID_DURATION); + }, FUTURE_RELEASE_DELAY); } }; @@ -92,7 +141,6 @@ public abstract class EventsTest super(BasicAnimatorActivity.class); } - /** * Sets up the fields used by each test. Subclasses must override this method to create * the protected mAnimator object used in all tests. Overrides must create that animator @@ -106,12 +154,21 @@ public abstract class EventsTest // mListener is the main testing mechanism of this file. The asserts of each test // are embedded in the listener callbacks that it implements. mListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // This should only be called on an animation that has not yet been started + assertFalse(mStarted); + assertTrue(mRunning); + mStarted = true; + } + @Override public void onAnimationCancel(Animator animation) { // This should only be called on an animation that has been started and not // yet canceled or ended assertFalse(mCanceled); assertTrue(mRunning); + assertTrue(mStarted); mCanceled = true; } @@ -120,7 +177,9 @@ public abstract class EventsTest // This should only be called on an animation that has been started and not // yet ended assertTrue(mRunning); + assertTrue(mStarted); mRunning = false; + mStarted = false; super.onAnimationEnd(animation); } }; @@ -132,6 +191,7 @@ public abstract class EventsTest mRunning = false; mCanceled = false; + mStarted = false; } /** @@ -143,27 +203,105 @@ public abstract class EventsTest mAnimator.cancel(); } + /** + * Verify that calling end on an unstarted animator does nothing. + */ + @UiThreadTest + @SmallTest + public void testEnd() throws Exception { + mAnimator.end(); + } + /** * Verify that calling cancel on a started animator does the right thing. */ @UiThreadTest @SmallTest public void testStartCancel() throws Exception { - mRunning = true; - mAnimator.start(); - mAnimator.cancel(); + mFutureListener = new FutureReleaseListener(mFuture); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + mRunning = true; + mAnimator.start(); + mAnimator.cancel(); + mFuture.release(); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + } + + /** + * Verify that calling end on a started animator does the right thing. + */ + @UiThreadTest + @SmallTest + public void testStartEnd() throws Exception { + mFutureListener = new FutureReleaseListener(mFuture); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + mRunning = true; + mAnimator.start(); + mAnimator.end(); + mFuture.release(); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); } /** * Same as testStartCancel, but with a startDelayed animator */ - @UiThreadTest @SmallTest public void testStartDelayedCancel() throws Exception { + mFutureListener = new FutureReleaseListener(mFuture); mAnimator.setStartDelay(ANIM_DELAY); - mRunning = true; - mAnimator.start(); - mAnimator.cancel(); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + mRunning = true; + mAnimator.start(); + mAnimator.cancel(); + mFuture.release(); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + } + + /** + * Same as testStartEnd, but with a startDelayed animator + */ + @SmallTest + public void testStartDelayedEnd() throws Exception { + mFutureListener = new FutureReleaseListener(mFuture); + mAnimator.setStartDelay(ANIM_DELAY); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + mRunning = true; + mAnimator.start(); + mAnimator.end(); + mFuture.release(); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); } /** @@ -180,13 +318,36 @@ public abstract class EventsTest mAnimator.addListener(mFutureListener); mRunning = true; mAnimator.start(); - handler.postDelayed(new Canceler(mAnimator), ANIM_MID_DURATION); + handler.postDelayed(new Canceler(mAnimator, mFuture), ANIM_MID_DURATION); } catch (junit.framework.AssertionFailedError e) { mFuture.setException(new RuntimeException(e)); } } }); - mFuture.get(); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + } + + /** + * Verify that ending an animator that is playing does the right thing. + */ + @MediumTest + public void testPlayingEnd() 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 Ender(mAnimator, mFuture), ANIM_MID_DURATION); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); } /** @@ -204,13 +365,91 @@ public abstract class EventsTest mAnimator.addListener(mFutureListener); mRunning = true; mAnimator.start(); - handler.postDelayed(new Canceler(mAnimator), ANIM_MID_DURATION); + handler.postDelayed(new Canceler(mAnimator, mFuture), ANIM_MID_DURATION); } catch (junit.framework.AssertionFailedError e) { mFuture.setException(new RuntimeException(e)); } } }); - mFuture.get(); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + } + + /** + * Same as testPlayingEnd, but with a startDelayed animator + */ + @MediumTest + public void testPlayingDelayedEnd() 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 Ender(mAnimator, mFuture), ANIM_MID_DURATION); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + } + + /** + * Same as testPlayingDelayedCancel, but cancel during the startDelay period + */ + @MediumTest + public void testPlayingDelayedCancelMidDelay() throws Exception { + mAnimator.setStartDelay(ANIM_DELAY); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + // Set the listener to automatically timeout after an uncanceled animation + // would have finished. This tests to make sure that we're not calling + // the listeners with cancel/end callbacks since they won't be called + // with the start event. + mFutureListener = new FutureReleaseListener(mFuture, TIMEOUT); + Handler handler = new Handler(); + mRunning = true; + mAnimator.start(); + handler.postDelayed(new Canceler(mAnimator, mFuture), ANIM_MID_DELAY); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT + 100, TimeUnit.MILLISECONDS); + } + + /** + * Same as testPlayingDelayedEnd, but end during the startDelay period + */ + @MediumTest + public void testPlayingDelayedEndMidDelay() throws Exception { + mAnimator.setStartDelay(ANIM_DELAY); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + // Set the listener to automatically timeout after an uncanceled animation + // would have finished. This tests to make sure that we're not calling + // the listeners with cancel/end callbacks since they won't be called + // with the start event. + mFutureListener = new FutureReleaseListener(mFuture, TIMEOUT); + Handler handler = new Handler(); + mRunning = true; + mAnimator.start(); + handler.postDelayed(new Ender(mAnimator, mFuture), ANIM_MID_DELAY); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT + 100, TimeUnit.MILLISECONDS); } /** @@ -234,7 +473,31 @@ public abstract class EventsTest } } }); - mFuture.get(); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + } + + /** + * Verifies that ending a started animation after it has already been ended + * does nothing. + */ + @MediumTest + public void testStartDoubleEnd() throws Exception { + mFutureListener = new FutureReleaseListener(mFuture); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + mRunning = true; + mAnimator.start(); + mAnimator.end(); + mAnimator.end(); + mFuture.release(); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); } /** @@ -258,8 +521,31 @@ public abstract class EventsTest } } }); - mFuture.get(); - } + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + } + /** + * Same as testStartDoubleEnd, but with a startDelayed animator + */ + @MediumTest + public void testStartDelayedDoubleEnd() throws Exception { + mAnimator.setStartDelay(ANIM_DELAY); + mFutureListener = new FutureReleaseListener(mFuture); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + mRunning = true; + mAnimator.start(); + mAnimator.end(); + mAnimator.end(); + mFuture.release(); + } catch (junit.framework.AssertionFailedError e) { + mFuture.setException(new RuntimeException(e)); + } + } + }); + mFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + } }