From be19e030a14c8e398e8af97fa898ea80187704df Mon Sep 17 00:00:00 2001 From: Chet Haase Date: Fri, 15 Mar 2013 17:08:55 -0700 Subject: [PATCH] Add auto-cancel ability to ObjectAnimator Add a method that enables a new auto-cancel option to ObjectAnimator. When set, any ObjectAnimator (when started) will cause any running ObjectAnimator instance (with that flag set) that has the same target and properties to cancel() itself prior to starting the new one. Issue #7426129 Add auto-cancel to animators Change-Id: I586659c365289cdb9afb6c416bdbaf5630477149 --- api/current.txt | 1 + .../android/animation/ObjectAnimator.java | 73 ++++++- .../java/android/animation/ValueAnimator.java | 18 +- .../src/android/animation/AutoCancelTest.java | 201 ++++++++++++++++++ .../src/android/animation/FutureWaiter.java | 11 +- 5 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 core/tests/coretests/src/android/animation/AutoCancelTest.java diff --git a/api/current.txt b/api/current.txt index ad2a623e8144e..c8b4cadb3bae4 100644 --- a/api/current.txt +++ b/api/current.txt @@ -2445,6 +2445,7 @@ package android.animation { method public static android.animation.ObjectAnimator ofObject(java.lang.Object, java.lang.String, android.animation.TypeEvaluator, java.lang.Object...); method public static android.animation.ObjectAnimator ofObject(T, android.util.Property, android.animation.TypeEvaluator, V...); method public static android.animation.ObjectAnimator ofPropertyValuesHolder(java.lang.Object, android.animation.PropertyValuesHolder...); + method public void setAutoCancel(boolean); method public void setProperty(android.util.Property); method public void setPropertyName(java.lang.String); } diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index 0372cb0d9cbe7..173ee73f337cd 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -19,7 +19,6 @@ package android.animation; import android.util.Log; import android.util.Property; -import java.lang.reflect.Method; import java.util.ArrayList; /** @@ -49,6 +48,8 @@ public final class ObjectAnimator extends ValueAnimator { private Property mProperty; + private boolean mAutoCancel = false; + /** * Sets the name of the property that will be animated. This name is used to derive * a setter function that will be called to set animated values. @@ -346,17 +347,83 @@ public final class ObjectAnimator extends ValueAnimator { // No values yet - this animator is being constructed piecemeal. Init the values with // whatever the current propertyName is if (mProperty != null) { - setValues(PropertyValuesHolder.ofObject(mProperty, (TypeEvaluator)null, values)); + setValues(PropertyValuesHolder.ofObject(mProperty, (TypeEvaluator) null, values)); } else { - setValues(PropertyValuesHolder.ofObject(mPropertyName, (TypeEvaluator)null, values)); + setValues(PropertyValuesHolder.ofObject(mPropertyName, + (TypeEvaluator) null, values)); } } else { super.setObjectValues(values); } } + /** + * autoCancel controls whether an ObjectAnimator will be canceled automatically + * when any other ObjectAnimator with the same target and properties is started. + * Setting this flag may make it easier to run different animators on the same target + * object without having to keep track of whether there are conflicting animators that + * need to be manually canceled. Canceling animators must have the same exact set of + * target properties, in the same order. + * + * @param cancel Whether future ObjectAnimators with the same target and properties + * as this ObjectAnimator will cause this ObjectAnimator to be canceled. + */ + public void setAutoCancel(boolean cancel) { + mAutoCancel = cancel; + } + + private boolean hasSameTargetAndProperties(Animator anim) { + if (anim instanceof ObjectAnimator) { + PropertyValuesHolder[] theirValues = ((ObjectAnimator) anim).getValues(); + if (((ObjectAnimator) anim).getTarget() == mTarget && + mValues.length == theirValues.length) { + for (int i = 0; i < mValues.length; ++i) { + PropertyValuesHolder pvhMine = mValues[i]; + PropertyValuesHolder pvhTheirs = theirValues[i]; + if (pvhMine.getPropertyName() == null || + !pvhMine.getPropertyName().equals(pvhTheirs.getPropertyName())) { + return false; + } + } + return true; + } + } + return false; + } + @Override public void start() { + // See if any of the current active/pending animators need to be canceled + AnimationHandler handler = sAnimationHandler.get(); + if (handler != null) { + int numAnims = handler.mAnimations.size(); + for (int i = numAnims - 1; i >= 0; i--) { + if (handler.mAnimations.get(i) instanceof ObjectAnimator) { + ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i); + if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { + anim.cancel(); + } + } + } + numAnims = handler.mPendingAnimations.size(); + for (int i = numAnims - 1; i >= 0; i--) { + if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) { + ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i); + if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { + anim.cancel(); + } + } + } + numAnims = handler.mDelayedAnims.size(); + for (int i = numAnims - 1; i >= 0; i--) { + if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) { + ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i); + if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { + anim.cancel(); + } + } + } + } if (DBG) { Log.d("ObjectAnimator", "Anim target, duration: " + mTarget + ", " + getDuration()); for (int i = 0; i < mValues.length; ++i) { diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 4a58072ae9fb9..ea605b9424f2b 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -83,7 +83,10 @@ public class ValueAnimator extends Animator { // The static sAnimationHandler processes the internal timing loop on which all animations // are based - private static ThreadLocal sAnimationHandler = + /** + * @hide + */ + protected static ThreadLocal sAnimationHandler = new ThreadLocal(); // The time interpolator to be used if none is set on the animation @@ -531,22 +534,27 @@ public class ValueAnimator extends Animator { * animations possible. * * The handler uses the Choreographer for executing periodic callbacks. + * + * @hide */ - private static class AnimationHandler implements Runnable { + protected static class AnimationHandler implements Runnable { // The per-thread list of all active animations - private final ArrayList mAnimations = new ArrayList(); + /** @hide */ + protected final ArrayList mAnimations = new ArrayList(); // Used in doAnimationFrame() to avoid concurrent modifications of mAnimations private final ArrayList mTmpAnimations = new ArrayList(); // The per-thread set of animations to be started on the next animation frame - private final ArrayList mPendingAnimations = new ArrayList(); + /** @hide */ + protected final ArrayList mPendingAnimations = new ArrayList(); /** * Internal per-thread collections used to avoid set collisions as animations start and end * while being processed. + * @hide */ - private final ArrayList mDelayedAnims = new ArrayList(); + protected final ArrayList mDelayedAnims = new ArrayList(); private final ArrayList mEndingAnims = new ArrayList(); private final ArrayList mReadyAnims = new ArrayList(); diff --git a/core/tests/coretests/src/android/animation/AutoCancelTest.java b/core/tests/coretests/src/android/animation/AutoCancelTest.java new file mode 100644 index 0000000000000..b1f88db86473d --- /dev/null +++ b/core/tests/coretests/src/android/animation/AutoCancelTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2013 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.suitebuilder.annotation.SmallTest; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +public class AutoCancelTest extends ActivityInstrumentationTestCase2 { + + boolean mAnimX1Canceled = false; + boolean mAnimXY1Canceled = false; + boolean mAnimX2Canceled = false; + boolean mAnimXY2Canceled = false; + + private static final long START_DELAY = 100; + private static final long DELAYED_START_DURATION = 200; + private static final long FUTURE_TIMEOUT = 1000; + + HashMap mCanceledMap = new HashMap(); + + public AutoCancelTest() { + super(BasicAnimatorActivity.class); + } + + ObjectAnimator setupAnimator(long startDelay, String... properties) { + ObjectAnimator returnVal; + if (properties.length == 1) { + returnVal = ObjectAnimator.ofFloat(this, properties[0], 0, 1); + } else { + PropertyValuesHolder[] pvhArray = new PropertyValuesHolder[properties.length]; + for (int i = 0; i < properties.length; i++) { + pvhArray[i] = PropertyValuesHolder.ofFloat(properties[i], 0, 1); + } + returnVal = ObjectAnimator.ofPropertyValuesHolder(this, pvhArray); + } + returnVal.setAutoCancel(true); + returnVal.setStartDelay(startDelay); + returnVal.addListener(mCanceledListener); + return returnVal; + } + + private void setupAnimators(long startDelay, boolean startLater, final FutureWaiter future) + throws Exception { + // Animators to be auto-canceled + final ObjectAnimator animX1 = setupAnimator(startDelay, "x"); + final ObjectAnimator animY1 = setupAnimator(startDelay, "y"); + final ObjectAnimator animXY1 = setupAnimator(startDelay, "x", "y"); + final ObjectAnimator animXZ1 = setupAnimator(startDelay, "x", "z"); + + animX1.start(); + animY1.start(); + animXY1.start(); + animXZ1.start(); + + final ObjectAnimator animX2 = setupAnimator(0, "x"); + animX2.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // We expect only animX1 to be canceled at this point + if (mCanceledMap.get(animX1) == null || + mCanceledMap.get(animX1) != true || + mCanceledMap.get(animY1) != null || + mCanceledMap.get(animXY1) != null || + mCanceledMap.get(animXZ1) != null) { + future.set(false); + } + } + }); + + final ObjectAnimator animXY2 = setupAnimator(0, "x", "y"); + animXY2.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // We expect only animXY1 to be canceled at this point + if (mCanceledMap.get(animXY1) == null || + mCanceledMap.get(animXY1) != true || + mCanceledMap.get(animY1) != null || + mCanceledMap.get(animXZ1) != null) { + future.set(false); + } + + } + + @Override + public void onAnimationEnd(Animator animation) { + // Release future if not done already via failures during start + future.release(); + } + }); + + if (startLater) { + Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + animX2.start(); + animXY2.start(); + } + }, DELAYED_START_DURATION); + } else { + animX2.start(); + animXY2.start(); + } + } + + @SmallTest + public void testAutoCancel() throws Exception { + final FutureWaiter future = new FutureWaiter(); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + setupAnimators(0, false, future); + } catch (Exception e) { + future.setException(e); + } + } + }); + assertTrue(future.get(FUTURE_TIMEOUT, TimeUnit.MILLISECONDS)); + } + + @SmallTest + public void testAutoCancelDelayed() throws Exception { + final FutureWaiter future = new FutureWaiter(); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + setupAnimators(START_DELAY, false, future); + } catch (Exception e) { + future.setException(e); + } + } + }); + assertTrue(future.get(FUTURE_TIMEOUT, TimeUnit.MILLISECONDS)); + } + + @SmallTest + public void testAutoCancelTestLater() throws Exception { + final FutureWaiter future = new FutureWaiter(); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + setupAnimators(0, true, future); + } catch (Exception e) { + future.setException(e); + } + } + }); + assertTrue(future.get(FUTURE_TIMEOUT, TimeUnit.MILLISECONDS)); + } + + @SmallTest + public void testAutoCancelDelayedTestLater() throws Exception { + final FutureWaiter future = new FutureWaiter(); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + setupAnimators(START_DELAY, true, future); + } catch (Exception e) { + future.setException(e); + } + } + }); + assertTrue(future.get(FUTURE_TIMEOUT, TimeUnit.MILLISECONDS)); + } + + private AnimatorListenerAdapter mCanceledListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + mCanceledMap.put(animation, true); + } + }; + + public void setX(float x) {} + + public void setY(float y) {} + + public void setZ(float z) {} +} + + diff --git a/core/tests/coretests/src/android/animation/FutureWaiter.java b/core/tests/coretests/src/android/animation/FutureWaiter.java index 320a1c281f04b..0c65e20be6a53 100644 --- a/core/tests/coretests/src/android/animation/FutureWaiter.java +++ b/core/tests/coretests/src/android/animation/FutureWaiter.java @@ -23,14 +23,21 @@ import com.google.common.util.concurrent.AbstractFuture; * {@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 { +public class FutureWaiter extends AbstractFuture { /** * Release the Future currently waiting on * {@link com.google.common.util.concurrent.AbstractFuture#get()}. */ public void release() { - super.set(null); + super.set(true); + } + + /** + * Used to indicate failure (when the result value is false). + */ + public void set(boolean result) { + super.set(result); } @Override