Fix Animator cancel() behavior

Previously, calling cancel() on an Animator would cause onAnimationCancel
events to be sent to all listeners. This was confusing for listeners
that were keying off this event for performing other actions, when the original
animator wasn't truly canceled (because it wasn't even running, or had already
been canceled earlier). This change hinges listener notification on
the animator actually running; no events are sent otherwise.

Also added the first set of Animator tests to verify that this behavior
is correct.

Change-Id: I81ab56559b5c0343c8dc7880e1c8235f3020975b
This commit is contained in:
Chet Haase
2011-07-11 17:01:56 -07:00
parent 221bfab842
commit 7dfacdb1c8
11 changed files with 547 additions and 72 deletions

View File

@@ -87,11 +87,13 @@ public final class AnimatorSet extends Animator {
private AnimatorSetListener mSetListener = null;
/**
* Flag indicating that the AnimatorSet has been canceled (by calling cancel() or end()).
* Flag indicating that the AnimatorSet has been manually
* terminated (by calling cancel() or end()).
* This flag is used to avoid starting other animations when currently-playing
* child animations of this AnimatorSet end.
* child animations of this AnimatorSet end. It also determines whether cancel/end
* notifications are sent out via the normal AnimatorSetListener mechanism.
*/
boolean mCanceled = false;
boolean mTerminated = false;
// The amount of time in ms to delay starting the animation after start() is called
private long mStartDelay = 0;
@@ -271,31 +273,29 @@ public final class AnimatorSet extends Animator {
@SuppressWarnings("unchecked")
@Override
public void cancel() {
mCanceled = true;
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
for (AnimatorListener listener : tmpListeners) {
listener.onAnimationCancel(this);
}
}
if (mDelayAnim != null && mDelayAnim.isRunning()) {
// If we're currently in the startDelay period, just cancel that animator and
// send out the end event to all listeners
mDelayAnim.cancel();
mTerminated = true;
if (isRunning()) {
ArrayList<AnimatorListener> tmpListeners = null;
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone();
for (AnimatorListener listener : tmpListeners) {
listener.onAnimationCancel(this);
}
}
if (mDelayAnim != null && mDelayAnim.isRunning()) {
// If we're currently in the startDelay period, just cancel that animator and
// send out the end event to all listeners
mDelayAnim.cancel();
} else if (mSortedNodes.size() > 0) {
for (Node node : mSortedNodes) {
node.animation.cancel();
}
}
if (tmpListeners != null) {
for (AnimatorListener listener : tmpListeners) {
listener.onAnimationEnd(this);
}
}
return;
}
if (mSortedNodes.size() > 0) {
for (Node node : mSortedNodes) {
node.animation.cancel();
}
}
}
@@ -307,23 +307,32 @@ public final class AnimatorSet extends Animator {
*/
@Override
public void end() {
mCanceled = true;
if (mSortedNodes.size() != mNodes.size()) {
// hasn't been started yet - sort the nodes now, then end them
sortNodes();
for (Node node : mSortedNodes) {
if (mSetListener == null) {
mSetListener = new AnimatorSetListener(this);
mTerminated = true;
if (isRunning()) {
if (mSortedNodes.size() != mNodes.size()) {
// hasn't been started yet - sort the nodes now, then end them
sortNodes();
for (Node node : mSortedNodes) {
if (mSetListener == null) {
mSetListener = new AnimatorSetListener(this);
}
node.animation.addListener(mSetListener);
}
node.animation.addListener(mSetListener);
}
}
if (mDelayAnim != null) {
mDelayAnim.cancel();
}
if (mSortedNodes.size() > 0) {
for (Node node : mSortedNodes) {
node.animation.end();
if (mDelayAnim != null) {
mDelayAnim.cancel();
}
if (mSortedNodes.size() > 0) {
for (Node node : mSortedNodes) {
node.animation.end();
}
}
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
for (AnimatorListener listener : tmpListeners) {
listener.onAnimationEnd(this);
}
}
}
}
@@ -424,7 +433,7 @@ public final class AnimatorSet extends Animator {
@SuppressWarnings("unchecked")
@Override
public void start() {
mCanceled = false;
mTerminated = false;
// First, sort the nodes (if necessary). This will ensure that sortedNodes
// contains the animation nodes in the correct order.
@@ -437,7 +446,8 @@ public final class AnimatorSet extends Animator {
ArrayList<AnimatorListener> oldListeners = node.animation.getListeners();
if (oldListeners != null && oldListeners.size() > 0) {
for (AnimatorListener listener : oldListeners) {
if (listener instanceof DependencyListener) {
if (listener instanceof DependencyListener ||
listener instanceof AnimatorSetListener) {
node.animation.removeListener(listener);
}
}
@@ -522,7 +532,7 @@ public final class AnimatorSet extends Animator {
* and will populate any appropriate lists, when it is started.
*/
anim.mNeedsSort = true;
anim.mCanceled = false;
anim.mTerminated = false;
anim.mPlayingSet = new ArrayList<Animator>();
anim.mNodeMap = new HashMap<Animator, Node>();
anim.mNodes = new ArrayList<Node>();
@@ -640,7 +650,7 @@ public final class AnimatorSet extends Animator {
* @param dependencyAnimation the animation that sent the event.
*/
private void startIfReady(Animator dependencyAnimation) {
if (mAnimatorSet.mCanceled) {
if (mAnimatorSet.mTerminated) {
// if the parent AnimatorSet was canceled, then don't start any dependent anims
return;
}
@@ -676,11 +686,15 @@ public final class AnimatorSet extends Animator {
}
public void onAnimationCancel(Animator animation) {
if (mPlayingSet.size() == 0) {
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationCancel(mAnimatorSet);
if (!mTerminated) {
// Listeners are already notified of the AnimatorSet canceling in cancel().
// The logic below only kicks in when animations end normally
if (mPlayingSet.size() == 0) {
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationCancel(mAnimatorSet);
}
}
}
}
@@ -692,24 +706,28 @@ public final class AnimatorSet extends Animator {
mPlayingSet.remove(animation);
Node animNode = mAnimatorSet.mNodeMap.get(animation);
animNode.done = true;
ArrayList<Node> sortedNodes = mAnimatorSet.mSortedNodes;
boolean allDone = true;
int numSortedNodes = sortedNodes.size();
for (int i = 0; i < numSortedNodes; ++i) {
if (!sortedNodes.get(i).done) {
allDone = false;
break;
if (!mTerminated) {
// Listeners are already notified of the AnimatorSet ending in cancel() or
// end(); the logic below only kicks in when animations end normally
ArrayList<Node> sortedNodes = mAnimatorSet.mSortedNodes;
boolean allDone = true;
int numSortedNodes = sortedNodes.size();
for (int i = 0; i < numSortedNodes; ++i) {
if (!sortedNodes.get(i).done) {
allDone = false;
break;
}
}
}
if (allDone) {
// If this was the last child animation to end, then notify listeners that this
// AnimatorSet has ended
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationEnd(mAnimatorSet);
if (allDone) {
// If this was the last child animation to end, then notify listeners that this
// AnimatorSet has ended
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationEnd(mAnimatorSet);
}
}
}
}
@@ -791,6 +809,8 @@ public final class AnimatorSet extends Animator {
}
}
}
// nodes are 'done' by default; they become un-done when started, and done
// again when ended
node.done = false;
}
}

View File

@@ -933,17 +933,17 @@ public class ValueAnimator extends Animator {
@Override
public void cancel() {
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
for (AnimatorListener listener : tmpListeners) {
listener.onAnimationCancel(this);
}
}
// Only cancel if the animation is actually running or has been started and is about
// to run
if (mPlayingState != STOPPED || sPendingAnimations.get().contains(this) ||
sDelayedAnims.get().contains(this)) {
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
for (AnimatorListener listener : tmpListeners) {
listener.onAnimationCancel(this);
}
}
endAnimation();
}
}

View File

@@ -12,7 +12,7 @@ LOCAL_SRC_FILES := \
$(call all-java-files-under, EnabledTestApp/src)
LOCAL_DX_FLAGS := --core-library
LOCAL_STATIC_JAVA_LIBRARIES := core-tests android-common frameworks-core-util-lib mockwebserver
LOCAL_STATIC_JAVA_LIBRARIES := core-tests android-common frameworks-core-util-lib mockwebserver guava
LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_PACKAGE_NAME := FrameworksCoreTests

View File

@@ -1235,6 +1235,13 @@
</intent-filter>
</activity>
<activity android:name="android.animation.BasicAnimatorActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
</intent-filter>
</activity>
</application>
<instrumentation android:name="android.test.InstrumentationTestRunner"

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/animatingButton"/>
</LinearLayout>

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.animation;
import android.widget.Button;
import com.android.frameworks.coretests.R;
/**
* Listener tests for AnimatorSet.
*/
public class AnimatorSetEventsTest extends EventsTest {
@Override
public void setUp() throws Exception {
final BasicAnimatorActivity activity = getActivity();
Button button = (Button) activity.findViewById(R.id.animatingButton);
ObjectAnimator xAnim = ObjectAnimator.ofFloat(button, "translationX", 0, 100);
ObjectAnimator yAnim = ObjectAnimator.ofFloat(button, "translationX", 0, 100);
mAnimator = new AnimatorSet();
((AnimatorSet)mAnimator).playSequentially(xAnim, yAnim);
super.setUp();
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.animation;
import com.android.frameworks.coretests.R;
import android.app.Activity;
import android.os.Bundle;
public class BasicAnimatorActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.animator_basic);
}
}

View File

@@ -0,0 +1,265 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.animation;
import android.os.Handler;
import android.test.ActivityInstrumentationTestCase2;
import android.test.UiThreadTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
/**
* 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
* testing mechanisms are in this class; the subclasses are only responsible for providing
* the mAnimator object.
*
* This test is more complicated than a typical synchronous test because much of the functionality
* must happen on the UI thread. Some tests do this by using the UiThreadTest annotation to
* automatically run the whole test on that thread. Other tests must run on the UI thread and also
* wait for some later event to occur before ending. These tests use a combination of an
* AbstractFuture mechanism and a delayed action to release that Future later.
*/
public abstract class EventsTest
extends ActivityInstrumentationTestCase2<BasicAnimatorActivity> {
private static final int ANIM_DURATION = 400;
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 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
private FutureWaiter mFuture; // Mechanism for waiting for the UI test to complete
private Animator.AnimatorListener mListener; // Listener that handles/tests the events
protected Animator mAnimator; // The animator used in the tests. Must be set in subclass
// setup() method prior to calling the superclass setup()
/**
* Cancels the given animator. Used to delay cancelation until some later time (after the
* animator has started playing).
*/
static class Canceler implements Runnable {
Animator mAnim;
public Canceler(Animator anim) {
mAnim = anim;
}
@Override
public void run() {
mAnim.cancel();
}
};
/**
* 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
* after an animation ends.
*/
static class FutureReleaseListener extends AnimatorListenerAdapter {
FutureWaiter mFuture;
public FutureReleaseListener(FutureWaiter future) {
mFuture = future;
}
@Override
public void onAnimationEnd(Animator animation) {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
mFuture.release();
}
}, ANIM_MID_DURATION);
}
};
public 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
* and then call super.setup(), where further properties are set on that animator.
* @throws Exception
*/
@Override
public void setUp() throws Exception {
super.setUp();
// 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 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);
mCanceled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
// This should only be called on an animation that has been started and not
// yet ended
assertTrue(mRunning);
mRunning = false;
super.onAnimationEnd(animation);
}
};
mAnimator.addListener(mListener);
mAnimator.setDuration(ANIM_DURATION);
mFuture = new FutureWaiter();
mRunning = false;
mCanceled = false;
}
/**
* Verify that calling cancel on an unstarted animator does nothing.
*/
@UiThreadTest
@SmallTest
public void testCancel() throws Exception {
mAnimator.cancel();
}
/**
* 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();
}
/**
* Same as testStartCancel, but with a startDelayed animator
*/
@UiThreadTest
@SmallTest
public void testStartDelayedCancel() throws Exception {
mAnimator.setStartDelay(ANIM_DELAY);
mRunning = true;
mAnimator.start();
mAnimator.cancel();
}
/**
* Verify that canceling an animator that is playing does the right thing.
*/
@MediumTest
public void testPlayingCancel() 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 Canceler(mAnimator), ANIM_MID_DURATION);
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
});
mFuture.get();
}
/**
* Same as testPlayingCancel, but with a startDelayed animator
*/
@MediumTest
public void testPlayingDelayedCancel() 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 Canceler(mAnimator), ANIM_MID_DURATION);
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
});
mFuture.get();
}
/**
* Verifies that canceling a started animation after it has already been canceled
* does nothing.
*/
@MediumTest
public void testStartDoubleCancel() throws Exception {
mFutureListener = new FutureReleaseListener(mFuture);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
mRunning = true;
mAnimator.start();
mAnimator.cancel();
mAnimator.cancel();
mFuture.release();
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
});
mFuture.get();
}
/**
* Same as testStartDoubleCancel, but with a startDelayed animator
*/
@MediumTest
public void testStartDelayedDoubleCancel() throws Exception {
mAnimator.setStartDelay(ANIM_DELAY);
mFutureListener = new FutureReleaseListener(mFuture);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
mRunning = true;
mAnimator.start();
mAnimator.cancel();
mAnimator.cancel();
mFuture.release();
} catch (junit.framework.AssertionFailedError e) {
mFuture.setException(new RuntimeException(e));
}
}
});
mFuture.get();
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.animation;
import com.google.common.util.concurrent.AbstractFuture;
/**
* Simple extension of {@link com.google.common.util.concurrent.AbstractFuture} which exposes a new
* release() method which calls the protected
* {@link com.google.common.util.concurrent.AbstractFuture#set(Object)} method internally. It
* also exposes the protected {@link AbstractFuture#setException(Throwable)} method.
*/
public class FutureWaiter extends AbstractFuture<Void> {
/**
* Release the Future currently waiting on
* {@link com.google.common.util.concurrent.AbstractFuture#get()}.
*/
public void release() {
super.set(null);
}
@Override
public boolean setException(Throwable throwable) {
return super.setException(throwable);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.animation;
import android.widget.Button;
import com.android.frameworks.coretests.R;
/**
* Listener tests for ObjectAnimator.
*/
public class ObjectAnimatorEventsTest extends EventsTest {
@Override
public void setUp() throws Exception {
final BasicAnimatorActivity activity = getActivity();
Button button = (Button) activity.findViewById(R.id.animatingButton);
mAnimator = ObjectAnimator.ofFloat(button, "translationX", 0, 100);
super.setUp();
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.animation;
/**
* Listener tests for ValueAnimator.
*/
public class ValueAnimatorEventsTest extends EventsTest {
@Override
public void setUp() throws Exception {
mAnimator = ValueAnimator.ofFloat(0, 1);
super.setUp();
}
}