Made it easier to use updatable Animators

The existing system is extended such that AnimationProperties
can be easier animated and running animations updated.

As a first sample this animates the scale of the
icons in the shelf.

Change-Id: Ic88e8094d53f37ab13f5e9e00796b63d229a5114
Test: runtest systemui
Bug: 32437839
(cherry picked from commit f082fe2319884737225952547bbaf5fc02359fc6)
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 8c80c71..2a895f9 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -20,12 +20,16 @@
     <item type="id" name="translation_y_animator_tag"/>
     <item type="id" name="translation_z_animator_tag"/>
     <item type="id" name="alpha_animator_tag"/>
+    <item type="id" name="scale_x_animator_tag"/>
+    <item type="id" name="scale_y_animator_tag"/>
     <item type="id" name="top_inset_animator_tag"/>
     <item type="id" name="height_animator_tag"/>
     <item type="id" name="shadow_alpha_animator_tag"/>
     <item type="id" name="translation_x_animator_end_value_tag"/>
     <item type="id" name="translation_y_animator_end_value_tag"/>
     <item type="id" name="translation_z_animator_end_value_tag"/>
+    <item type="id" name="scale_x_animator_end_value_tag"/>
+    <item type="id" name="scale_y_animator_end_value_tag"/>
     <item type="id" name="alpha_animator_end_value_tag"/>
     <item type="id" name="top_inset_animator_end_value_tag"/>
     <item type="id" name="height_animator_end_value_tag"/>
@@ -33,6 +37,8 @@
     <item type="id" name="translation_x_animator_start_value_tag"/>
     <item type="id" name="translation_y_animator_start_value_tag"/>
     <item type="id" name="translation_z_animator_start_value_tag"/>
+    <item type="id" name="scale_x_animator_start_value_tag"/>
+    <item type="id" name="scale_y_animator_start_value_tag"/>
     <item type="id" name="alpha_animator_start_value_tag"/>
     <item type="id" name="top_inset_animator_start_value_tag"/>
     <item type="id" name="height_animator_start_value_tag"/>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java
new file mode 100644
index 0000000..80ba943
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 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 com.android.systemui.statusbar.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ViewState;
+
+/**
+ * An animator to animate properties
+ */
+public class PropertyAnimator {
+
+    public static <T extends View> void startAnimation(final T view,
+            AnimatableProperty animatableProperty, float newEndValue,
+            AnimationProperties properties) {
+        Property<T, Float> property = animatableProperty.getProperty();
+        int animationStartTag = animatableProperty.getAnimationStartTag();
+        int animationEndTag = animatableProperty.getAnimationEndTag();
+        Float previousStartValue = ViewState.getChildTag(view, animationStartTag);
+        Float previousEndValue = ViewState.getChildTag(view, animationEndTag);
+        if (previousEndValue != null && previousEndValue == newEndValue) {
+            return;
+        }
+        int animatorTag = animatableProperty.getAnimatorTag();
+        ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag);
+        AnimationFilter filter = properties.getAnimationFilter();
+        if (!filter.shouldAnimateProperty(property)) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                float relativeDiff = newEndValue - previousEndValue;
+                float newStartValue = previousStartValue + relativeDiff;
+                values[0].setFloatValues(newStartValue, newEndValue);
+                view.setTag(animationStartTag, newStartValue);
+                view.setTag(animationEndTag, newEndValue);
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                property.set(view, newEndValue);
+                return;
+            }
+        }
+
+        Float currentValue = property.get(view);
+        ValueAnimator animator = ValueAnimator.ofFloat(currentValue, newEndValue);
+        animator.addUpdateListener(
+                animation -> property.set(view, (Float) animation.getAnimatedValue()));
+        Interpolator customInterpolator = properties.getCustomInterpolator(view, property);
+        Interpolator interpolator =  customInterpolator != null ? customInterpolator
+                : Interpolators.FAST_OUT_SLOW_IN;
+        animator.setInterpolator(interpolator);
+        long newDuration = ViewState.cancelAnimatorAndGetNewDuration(properties.duration,
+                previousAnimator);
+        animator.setDuration(newDuration);
+        if (properties.delay > 0 && (previousAnimator == null
+                || previousAnimator.getAnimatedFraction() == 0)) {
+            animator.setStartDelay(properties.delay);
+        }
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+        if (listener != null) {
+            animator.addListener(listener);
+        }
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                view.setTag(animatorTag, null);
+                view.setTag(animationStartTag, null);
+                view.setTag(animationEndTag, null);
+            }
+        });
+        ViewState.startAnimator(animator, listener);
+        view.setTag(animatorTag, animator);
+        view.setTag(animationStartTag, currentValue);
+        view.setTag(animationEndTag, newEndValue);
+    }
+
+    public interface AnimatableProperty {
+        int getAnimationStartTag();
+        int getAnimationEndTag();
+        int getAnimatorTag();
+        Property getProperty();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 4b0f454..9fb5980 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -62,8 +62,8 @@
     }.setDuration(200);
 
     private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
-        private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha();
-        // TODO: add scale
+        private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha()
+                .animateScale();
 
         @Override
         public AnimationFilter getAnimationFilter() {
@@ -75,13 +75,12 @@
 
     private static final AnimationProperties mTempProperties = new AnimationProperties() {
         private AnimationFilter mAnimationFilter = new AnimationFilter();
-        // TODO: add scale
 
         @Override
         public AnimationFilter getAnimationFilter() {
             return mAnimationFilter;
         }
-    }.setDuration(CANNED_ANIMATION_DURATION);
+    };
 
     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
index 38bb40e..34fa658 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
@@ -16,7 +16,12 @@
 
 package com.android.systemui.statusbar.stack;
 
+import android.support.v4.util.ArraySet;
+import android.util.Property;
+import android.view.View;
+
 import java.util.ArrayList;
+import java.util.HashSet;
 
 /**
  * Filters the animations for only a certain type of properties.
@@ -37,12 +42,19 @@
     boolean hasDarkEvent;
     boolean hasHeadsUpDisappearClickEvent;
     int darkAnimationOriginIndex;
+    private ArraySet<Property> mAnimatedProperties = new ArraySet<>();
 
     public AnimationFilter animateAlpha() {
         animateAlpha = true;
         return this;
     }
 
+    public AnimationFilter animateScale() {
+        animate(View.SCALE_X);
+        animate(View.SCALE_Y);
+        return this;
+    }
+
     public AnimationFilter animateX() {
         animateX = true;
         return this;
@@ -132,6 +144,7 @@
         animateHideSensitive |= filter.animateHideSensitive;
         animateShadowAlpha |= filter.animateShadowAlpha;
         hasDelays |= filter.hasDelays;
+        mAnimatedProperties.addAll(filter.mAnimatedProperties);
     }
 
     public void reset() {
@@ -151,5 +164,16 @@
         hasHeadsUpDisappearClickEvent = false;
         darkAnimationOriginIndex =
                 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE;
+        mAnimatedProperties.clear();
+    }
+
+    public AnimationFilter animate(Property property) {
+        mAnimatedProperties.add(property);
+        return this;
+    }
+
+    public boolean shouldAnimateProperty(Property property) {
+        // TODO: migrate all existing animators to properties
+        return mAnimatedProperties.contains(property);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
index b747592..e0a6159 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
@@ -21,12 +21,14 @@
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
 import android.animation.ValueAnimator;
+import android.util.Property;
 import android.view.View;
 import android.view.animation.Interpolator;
 
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 /**
@@ -60,6 +62,54 @@
     private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
     private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
 
+    private static final PropertyAnimator.AnimatableProperty SCALE_X_PROPERTY
+            = new PropertyAnimator.AnimatableProperty() {
+
+        @Override
+        public int getAnimationStartTag() {
+            return R.id.scale_x_animator_start_value_tag;
+        }
+
+        @Override
+        public int getAnimationEndTag() {
+            return R.id.scale_x_animator_end_value_tag;
+        }
+
+        @Override
+        public int getAnimatorTag() {
+            return R.id.scale_x_animator_tag;
+        }
+
+        @Override
+        public Property getProperty() {
+            return View.SCALE_X;
+        }
+    };
+
+    private static final PropertyAnimator.AnimatableProperty SCALE_Y_PROPERTY
+            = new PropertyAnimator.AnimatableProperty() {
+
+        @Override
+        public int getAnimationStartTag() {
+            return R.id.scale_y_animator_start_value_tag;
+        }
+
+        @Override
+        public int getAnimationEndTag() {
+            return R.id.scale_y_animator_end_value_tag;
+        }
+
+        @Override
+        public int getAnimatorTag() {
+            return R.id.scale_y_animator_tag;
+        }
+
+        @Override
+        public Property getProperty() {
+            return View.SCALE_Y;
+        }
+    };
+
     public float alpha;
     public float xTranslation;
     public float yTranslation;
@@ -125,13 +175,19 @@
         }
 
         // apply scaleX
-        if (view.getScaleX() != this.scaleX) {
-            view.setScaleX(this.scaleX);
+        boolean animatingScaleX = isAnimating(view, SCALE_X_PROPERTY);
+        if (animatingScaleX) {
+            updateAnimation(view, SCALE_X_PROPERTY, scaleX);
+        } else if (view.getScaleX() != scaleX) {
+            view.setScaleX(scaleX);
         }
 
         // apply scaleY
-        if (view.getScaleY() != this.scaleY) {
-            view.setScaleY(this.scaleY);
+        boolean animatingScaleY = isAnimating(view, SCALE_Y_PROPERTY);
+        if (animatingScaleY) {
+            updateAnimation(view, SCALE_Y_PROPERTY, scaleY);
+        } else if (view.getScaleY() != scaleY) {
+            view.setScaleY(scaleY);
         }
 
         boolean becomesInvisible = this.alpha == 0.0f || (this.hidden && !isAnimating(view));
@@ -179,13 +235,23 @@
         if (isAnimating(view, TAG_ANIMATOR_ALPHA)) {
             return true;
         }
+        if (isAnimating(view, SCALE_X_PROPERTY)) {
+            return true;
+        }
+        if (isAnimating(view, SCALE_Y_PROPERTY)) {
+            return true;
+        }
         return false;
     }
 
-    private boolean isAnimating(View view, int tag) {
+    private static boolean isAnimating(View view, int tag) {
         return getChildTag(view, tag) != null;
     }
 
+    public static boolean isAnimating(View view, PropertyAnimator.AnimatableProperty property) {
+        return getChildTag(view, property.getAnimatorTag()) != null;
+    }
+
     /**
      * Start an animation to this viewstate
      * @param child the view to animate
@@ -226,6 +292,20 @@
             abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Z);
         }
 
+        // start scaleX animation
+        if (child.getScaleX() != scaleX) {
+            PropertyAnimator.startAnimation(child, SCALE_X_PROPERTY, scaleX, animationProperties);
+        } else {
+            abortAnimation(child, SCALE_X_PROPERTY.getAnimatorTag());
+        }
+
+        // start scaleX animation
+        if (child.getScaleY() != scaleY) {
+            PropertyAnimator.startAnimation(child, SCALE_Y_PROPERTY, scaleY, animationProperties);
+        } else {
+            abortAnimation(child, SCALE_Y_PROPERTY.getAnimatorTag());
+        }
+
         // start alpha animation
         if (alphaChanging) {
             startAlphaAnimation(child, animationProperties);
@@ -320,6 +400,11 @@
         startZTranslationAnimation(view, NO_NEW_ANIMATIONS);
     }
 
+    private void updateAnimation(View view, PropertyAnimator.AnimatableProperty property,
+            float endValue) {
+        PropertyAnimator.startAnimation(view, property, endValue, NO_NEW_ANIMATIONS);
+    }
+
     private void startZTranslationAnimation(final View child, AnimationProperties properties) {
         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
@@ -514,7 +599,7 @@
         }
     }
 
-    protected void startAnimator(Animator animator, AnimatorListenerAdapter listener) {
+    public static void startAnimator(Animator animator, AnimatorListenerAdapter listener) {
         if (listener != null) {
             // Even if there's a delay we'd want to notify it of the start immediately.
             listener.onAnimationStart(animator);
@@ -540,7 +625,8 @@
      * @param previousAnimator the animator which was running before
      * @return the new duration
      */
-    protected long cancelAnimatorAndGetNewDuration(long duration, ValueAnimator previousAnimator) {
+    public static long cancelAnimatorAndGetNewDuration(long duration,
+            ValueAnimator previousAnimator) {
         long newDuration = duration;
         if (previousAnimator != null) {
             // We take either the desired length of the new animation or the remaining time of
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notification/PropertyAnimatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/notification/PropertyAnimatorTest.java
new file mode 100644
index 0000000..193250f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notification/PropertyAnimatorTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2016 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 com.android.systemui.notification;
+
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+import android.util.FloatProperty;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.PropertyAnimator;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ViewState;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PropertyAnimatorTest extends SysuiTestCase {
+
+    private View mView;
+    private FloatProperty<View> mEffectiveProperty = new FloatProperty<View>("TEST") {
+        public float mValue = 100;
+
+        @Override
+        public void setValue(View view, float value) {
+            mValue = value;
+        }
+
+        @Override
+        public Float get(View object) {
+            return mValue;
+        }
+    };
+    private PropertyAnimator.AnimatableProperty mProperty
+            = new PropertyAnimator.AnimatableProperty() {
+
+        @Override
+        public int getAnimationStartTag() {
+            return R.id.scale_x_animator_start_value_tag;
+        }
+
+        @Override
+        public int getAnimationEndTag() {
+            return R.id.scale_x_animator_end_value_tag;
+        }
+
+        @Override
+        public int getAnimatorTag() {
+            return R.id.scale_x_animator_tag;
+        }
+
+        @Override
+        public Property getProperty() {
+            return mEffectiveProperty;
+        }
+    };
+    private AnimatorListenerAdapter mFinishListener = mock(AnimatorListenerAdapter.class);
+    private AnimationProperties mAnimationProperties = new AnimationProperties() {
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return mAnimationFilter;
+        }
+
+        @Override
+        public AnimatorListenerAdapter getAnimationFinishListener() {
+            return mFinishListener;
+        }
+    }.setDuration(200);
+    private AnimationFilter mAnimationFilter = new AnimationFilter();
+    private Interpolator mTestInterpolator = Interpolators.ALPHA_IN;
+
+
+    @Before
+    @UiThreadTest
+    public void setUp() {
+        mView = new View(getContext());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testAnimationStarted() {
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        PropertyAnimator.startAnimation(mView, mProperty, 200, mAnimationProperties);
+        assertTrue(ViewState.isAnimating(mView, mProperty));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testNoAnimationStarted() {
+        mAnimationFilter.reset();
+        PropertyAnimator.startAnimation(mView, mProperty, 200, mAnimationProperties);
+        assertFalse(ViewState.isAnimating(mView, mProperty));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testEndValueUpdated() {
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationEndTag()),
+                Float.valueOf(200f));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testStartTagUpdated() {
+        mEffectiveProperty.set(mView, 100f);
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationStartTag()),
+                Float.valueOf(100f));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testValueIsSetUnAnimated() {
+        mAnimationFilter.reset();
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        assertEquals(Float.valueOf(200f), mEffectiveProperty.get(mView));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testAnimationToRightValueUpdated() {
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        mAnimationFilter.reset();
+        PropertyAnimator.startAnimation(mView, mProperty, 220f, mAnimationProperties);
+        assertTrue(ViewState.isAnimating(mView, mProperty));
+        assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationEndTag()),
+                Float.valueOf(220f));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testAnimationToRightValueUpdateAnimated() {
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        PropertyAnimator.startAnimation(mView, mProperty, 220f, mAnimationProperties);
+        assertTrue(ViewState.isAnimating(mView, mProperty));
+        assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationEndTag()),
+                Float.valueOf(220f));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testStartTagShiftedWhenChanging() {
+        mEffectiveProperty.set(mView, 100f);
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        mAnimationFilter.reset();
+        PropertyAnimator.startAnimation(mView, mProperty, 220f, mAnimationProperties);
+        assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationStartTag()),
+                Float.valueOf(120f));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testUsingDuration() {
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        mAnimationProperties.setDuration(500);
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag());
+        assertNotNull(animator);
+        assertEquals(animator.getDuration(), 500);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testUsingDelay() {
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        mAnimationProperties.setDelay(200);
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag());
+        assertNotNull(animator);
+        assertEquals(animator.getStartDelay(), 200);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testUsingInterpolator() {
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        mAnimationProperties.setCustomInterpolator(mEffectiveProperty, mTestInterpolator);
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag());
+        assertNotNull(animator);
+        assertEquals(animator.getInterpolator(), mTestInterpolator);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testUsingListener() {
+        mAnimationFilter.reset();
+        mAnimationFilter.animate(mProperty.getProperty());
+        mAnimationProperties.setCustomInterpolator(mEffectiveProperty, mTestInterpolator);
+        PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+        ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag());
+        assertNotNull(animator);
+        assertTrue(animator.getListeners().contains(mFinishListener));
+    }
+}