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
This commit is contained in:
Selim Cinek
2016-12-20 14:32:19 +01:00
parent 061d90729b
commit f082fe2319
6 changed files with 484 additions and 11 deletions

View File

@@ -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"/>

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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<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 @@ 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);
}
}

View File

@@ -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

View File

@@ -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));
}
}