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