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
diff --git a/api/current.txt b/api/current.txt
index ad2a623..c8b4cad 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2445,6 +2445,7 @@
     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<T, V>, android.animation.TypeEvaluator<V>, 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 0372cb0..173ee73 100644
--- a/core/java/android/animation/ObjectAnimator.java
+++ b/core/java/android/animation/ObjectAnimator.java
@@ -19,7 +19,6 @@
 import android.util.Log;
 import android.util.Property;
 
-import java.lang.reflect.Method;
 import java.util.ArrayList;
 
 /**
@@ -49,6 +48,8 @@
 
     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 @@
             // 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 4a58072..ea605b9 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -83,7 +83,10 @@
 
     // The static sAnimationHandler processes the internal timing loop on which all animations
     // are based
-    private static ThreadLocal<AnimationHandler> sAnimationHandler =
+    /**
+     * @hide
+     */
+    protected static ThreadLocal<AnimationHandler> sAnimationHandler =
             new ThreadLocal<AnimationHandler>();
 
     // The time interpolator to be used if none is set on the animation
@@ -531,22 +534,27 @@
      * 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<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();
+        /** @hide */
+        protected final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();
 
         // Used in doAnimationFrame() to avoid concurrent modifications of mAnimations
         private final ArrayList<ValueAnimator> mTmpAnimations = new ArrayList<ValueAnimator>();
 
         // The per-thread set of animations to be started on the next animation frame
-        private final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>();
+        /** @hide */
+        protected final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>();
 
         /**
          * Internal per-thread collections used to avoid set collisions as animations start and end
          * while being processed.
+         * @hide
          */
-        private final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>();
+        protected final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>();
         private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>();
         private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>();
 
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 0000000..b1f88db
--- /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<BasicAnimatorActivity> {
+
+    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<Animator, Boolean> mCanceledMap = new HashMap<Animator, Boolean>();
+
+    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 320a1c2..0c65e20 100644
--- a/core/tests/coretests/src/android/animation/FutureWaiter.java
+++ b/core/tests/coretests/src/android/animation/FutureWaiter.java
@@ -23,14 +23,21 @@
  * {@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> {
+public class FutureWaiter extends AbstractFuture<Boolean> {
 
     /**
      * 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