Merge "Fix Animator cancel() behavior"

This commit is contained in:
Chet Haase
2011-07-12 11:58:58 -07:00
committed by Android (Google) Code Review
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

@@ -1242,6 +1242,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();
}
}