Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit be19e030 authored by Chet Haase's avatar Chet Haase
Browse files

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
parent 63f1e2fb
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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<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);
  }
+70 −3
Original line number Diff line number Diff line
@@ -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.
@@ -348,15 +349,81 @@ public final class ObjectAnimator extends ValueAnimator {
            if (mProperty != null) {
                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) {
+13 −5
Original line number Diff line number Diff line
@@ -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<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 @@ 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<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>();

+201 −0
Original line number Diff line number Diff line
/*
 * 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) {}
}

+9 −2
Original line number Diff line number Diff line
@@ -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<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