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

Commit 75dfe46e authored by Chet Haase's avatar Chet Haase Committed by Android (Google) Code Review
Browse files

Merge "Add auto-cancel ability to ObjectAnimator" into jb-mr2-dev

parents 10c4d99f be19e030
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