Fix AnimatorSet cancellation issues

AnimatorSet was incorrectly ignoring cancel() when it was in the
initial startDelay phase. Fix is to change isRunning() to be true if the
animator is also in its delay phase.

Change-Id: I1a8c877de24fa294beea0ba30d495658255b13b3
This commit is contained in:
Chet Haase
2011-08-03 14:10:06 -07:00
parent 3f76ca47e2
commit b8f574a165
4 changed files with 327 additions and 28 deletions

View File

@@ -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.
*
* <p>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

View File

@@ -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() {

View File

@@ -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<AnimatorListener> 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<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) 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<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
@@ -1008,6 +1021,7 @@ public class ValueAnimator extends Animator {
tmpListeners.get(i).onAnimationEnd(this);
}
}
mStarted = false;
}
/**

View File

@@ -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);
}
}