Merge "Fix AnimatorSet cancellation issues"
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user