From f082fe2319884737225952547bbaf5fc02359fc6 Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Tue, 20 Dec 2016 14:32:19 +0100 Subject: [PATCH] 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 --- packages/SystemUI/res/values/ids.xml | 6 + .../notification/PropertyAnimator.java | 111 ++++++++ .../phone/NotificationIconContainer.java | 7 +- .../statusbar/stack/AnimationFilter.java | 24 ++ .../systemui/statusbar/stack/ViewState.java | 100 ++++++- .../notification/PropertyAnimatorTest.java | 247 ++++++++++++++++++ 6 files changed, 484 insertions(+), 11 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/notification/PropertyAnimatorTest.java diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 8c80c71f869e8..2a895f98eefdc 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -20,12 +20,16 @@ + + + + @@ -33,6 +37,8 @@ + + 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 0000000000000..80ba94398e1bb --- /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 void startAnimation(final T view, + AnimatableProperty animatableProperty, float newEndValue, + AnimationProperties properties) { + Property 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 4b0f4549bf9dd..9fb59801f8f0f 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 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { }.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 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { 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 38bb40eb4a10c..34fa658c85e1d 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 @@ public class AnimationFilter { boolean hasDarkEvent; boolean hasHeadsUpDisappearClickEvent; int darkAnimationOriginIndex; + private ArraySet 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 @@ public class AnimationFilter { animateHideSensitive |= filter.animateHideSensitive; animateShadowAlpha |= filter.animateShadowAlpha; hasDelays |= filter.hasDelays; + mAnimatedProperties.addAll(filter.mAnimatedProperties); } public void reset() { @@ -151,5 +164,16 @@ public class AnimationFilter { 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 b747592146691..e0a6159c4c035 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.AnimatorListenerAdapter; 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 @@ public class ViewState { 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 @@ public class ViewState { } // 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 @@ public class ViewState { 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 @@ public class ViewState { 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 @@ public class ViewState { 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 @@ public class ViewState { } } - 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 @@ public class ViewState { * @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 0000000000000..193250fe4e8f9 --- /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 mEffectiveProperty = new FloatProperty("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)); + } +}