Fix janky wakeup animation

Previous wake-up animation was totally broken before, as multiple
animations were running with different interpolators and different
durations, creating a really jaring experience for the front-door
of the phone, which users see hundreds of times a day.

Instead, we use a single animator to drive everything, and pass in
both the interpolated and the linear value so the right value can
be used in whichever place.

Test: Wakeup/sleep phone with different numbers of notifications
Fixes: 110980608
Change-Id: If1758404a4c49fcd7dc9fa3c93bb18e42a586632
This commit is contained in:
Jorim Jaggi
2018-07-05 18:28:12 +02:00
parent 5c75b5b60e
commit c4cf07a16f
3 changed files with 103 additions and 77 deletions

View File

@@ -31,6 +31,13 @@ import com.android.systemui.statusbar.stack.HeadsUpAppearInterpolator;
*/
public class Interpolators {
public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
/**
* Like {@link #FAST_OUT_SLOW_IN}, but used in case the animation is played in reverse (i.e. t
* goes from 1 to 0 instead of 0 to 1).
*/
public static final Interpolator FAST_OUT_SLOW_IN_REVERSE =
new PathInterpolator(0.8f, 0f, 0.6f, 1f);
public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
@@ -51,4 +58,11 @@ public class Interpolators {
*/
public static final Interpolator TOUCH_RESPONSE =
new PathInterpolator(0.3f, 0f, 0.1f, 1f);
/**
* Like {@link #TOUCH_RESPONSE}, but used in case the animation is played in reverse (i.e. t
* goes from 1 to 0 instead of 0 to 1).
*/
public static final Interpolator TOUCH_RESPONSE_REVERSE =
new PathInterpolator(0.9f, 0f, 0.7f, 1f);
}

View File

@@ -47,6 +47,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import com.android.internal.logging.MetricsLogger;
@@ -107,17 +108,20 @@ public class NotificationPanelView extends PanelView implements
private static final AnimationProperties CLOCK_ANIMATION_PROPERTIES = new AnimationProperties()
.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
private static final FloatProperty<NotificationPanelView> SET_DARK_AMOUNT_PROPERTY =
new FloatProperty<NotificationPanelView>("mDarkAmount") {
new FloatProperty<NotificationPanelView>("mInterpolatedDarkAmount") {
@Override
public void setValue(NotificationPanelView object, float value) {
object.setDarkAmount(value);
object.setDarkAmount(value, object.mDarkInterpolator.getInterpolation(value));
}
@Override
public Float get(NotificationPanelView object) {
return object.mDarkAmount;
return object.mLinearDarkAmount;
}
};
private Interpolator mDarkInterpolator;
private final PowerManager mPowerManager;
private final AccessibilityManager mAccessibilityManager;
@@ -237,7 +241,18 @@ public class NotificationPanelView extends PanelView implements
private int mIndicationBottomPadding;
private int mAmbientIndicationBottomPadding;
private boolean mIsFullWidth;
private float mDarkAmount;
/**
* Current dark amount that follows regular interpolation curve of animation.
*/
private float mInterpolatedDarkAmount;
/**
* Dark amount that animates from 0 to 1 or vice-versa in linear manner, even if the
* interpolation curve is different.
*/
private float mLinearDarkAmount;
private float mDarkAmountTarget;
private boolean mPulsing;
private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
@@ -392,7 +407,7 @@ public class NotificationPanelView extends PanelView implements
false);
addView(mKeyguardBottomArea, index);
initBottomArea();
setDarkAmount(mDarkAmount);
setDarkAmount(mLinearDarkAmount, mInterpolatedDarkAmount);
setKeyguardStatusViewVisibility(mStatusBarState, false, false);
setKeyguardBottomAreaVisibility(mStatusBarState, false);
@@ -506,7 +521,7 @@ public class NotificationPanelView extends PanelView implements
getExpandedFraction(),
totalHeight,
mKeyguardStatusView.getHeight(),
mDarkAmount,
mInterpolatedDarkAmount,
mStatusBar.isKeyguardCurrentlySecure(),
mPulsing,
mBouncerTop);
@@ -1917,7 +1932,7 @@ public class NotificationPanelView extends PanelView implements
if (view == null && mQsExpanded) {
return;
}
if (needsAnimation && mDarkAmount == 0) {
if (needsAnimation && mInterpolatedDarkAmount == 0) {
mAnimateNextPositionUpdate = true;
}
ExpandableView firstChildNotGone = mNotificationStackScroller.getFirstChildNotGone();
@@ -2727,20 +2742,28 @@ public class NotificationPanelView extends PanelView implements
}
mDarkAmountTarget = darkAmount;
if (animate) {
if (mInterpolatedDarkAmount == 0f || mInterpolatedDarkAmount == 1f) {
mDarkInterpolator = dozing
? Interpolators.FAST_OUT_SLOW_IN
: Interpolators.TOUCH_RESPONSE_REVERSE;
}
mNotificationStackScroller.notifyDarkAnimationStart(dozing);
mDarkAnimator = ObjectAnimator.ofFloat(this, SET_DARK_AMOUNT_PROPERTY, darkAmount);
mDarkAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
mDarkAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
mDarkAnimator.setInterpolator(Interpolators.LINEAR);
mDarkAnimator.setDuration(mNotificationStackScroller.getDarkAnimationDuration(dozing));
mDarkAnimator.start();
} else {
setDarkAmount(darkAmount);
setDarkAmount(darkAmount, darkAmount);
}
}
private void setDarkAmount(float amount) {
mDarkAmount = amount;
mKeyguardStatusView.setDarkAmount(mDarkAmount);
mKeyguardBottomArea.setDarkAmount(mDarkAmount);
private void setDarkAmount(float linearAmount, float amount) {
mInterpolatedDarkAmount = amount;
mLinearDarkAmount = linearAmount;
mKeyguardStatusView.setDarkAmount(mInterpolatedDarkAmount);
mKeyguardBottomArea.setDarkAmount(mInterpolatedDarkAmount);
positionClockAndNotifications();
mNotificationStackScroller.setDarkAmount(linearAmount, mInterpolatedDarkAmount);
}
public void setPulsing(boolean pulsing) {
@@ -2765,7 +2788,7 @@ public class NotificationPanelView extends PanelView implements
public void dozeTimeTick() {
mKeyguardStatusView.dozeTimeTick();
mKeyguardBottomArea.dozeTimeTick();
if (mDarkAmount > 0) {
if (mInterpolatedDarkAmount > 0) {
positionClockAndNotifications();
}
}

View File

@@ -34,7 +34,6 @@ import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
@@ -46,11 +45,9 @@ import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v4.graphics.ColorUtils;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Log;
import android.util.MathUtils;
import android.util.Pair;
import android.util.Property;
import android.view.ContextThemeWrapper;
import android.view.InputDevice;
import android.view.MotionEvent;
@@ -375,25 +372,22 @@ public class NotificationStackScrollLayout extends ViewGroup
private boolean mScrollable;
private View mForcedScroll;
private View mNeedingPulseAnimation;
private float mDarkAmount = 0f;
/**
* @see #setDarkAmount(float, float)
*/
private float mInterpolatedDarkAmount = 0f;
/**
* @see #setDarkAmount(float, float)
*/
private float mLinearDarkAmount = 0f;
/**
* How fast the background scales in the X direction as a factor of the Y expansion.
*/
private float mBackgroundXFactor = 1f;
private static final Property<NotificationStackScrollLayout, Float> DARK_AMOUNT =
new FloatProperty<NotificationStackScrollLayout>("darkAmount") {
@Override
public void setValue(NotificationStackScrollLayout object, float value) {
object.setDarkAmount(value);
}
@Override
public Float get(NotificationStackScrollLayout object) {
return object.getDarkAmount();
}
};
private ObjectAnimator mDarkAmountAnimator;
private boolean mUsingLightTheme;
private boolean mQsExpanded;
private boolean mForwardScrollable;
@@ -424,6 +418,8 @@ public class NotificationStackScrollLayout extends ViewGroup
private NotificationIconAreaController mIconAreaController;
private float mVerticalPanelTranslation;
private Interpolator mDarkXInterpolator = Interpolators.FAST_OUT_SLOW_IN;
public NotificationStackScrollLayout(Context context) {
this(context, null);
}
@@ -558,16 +554,16 @@ public class NotificationStackScrollLayout extends ViewGroup
canvas.drawRect(darkLeft, darkTop, darkRight, darkBottom, mBackgroundPaint);
}
} else {
float inverseDark = 1 - mDarkAmount;
float yProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(inverseDark);
float xProgress = Interpolators.FAST_OUT_SLOW_IN
.getInterpolation(inverseDark * mBackgroundXFactor);
float yProgress = 1 - mInterpolatedDarkAmount;
float xProgress = mDarkXInterpolator.getInterpolation(
(1 - mLinearDarkAmount) * mBackgroundXFactor);
mBackgroundAnimationRect.set(
(int) MathUtils.lerp(darkLeft, lockScreenLeft, xProgress),
(int) MathUtils.lerp(darkTop, lockScreenTop, yProgress),
(int) MathUtils.lerp(darkRight, lockScreenRight, xProgress),
(int) MathUtils.lerp(darkBottom, lockScreenBottom, yProgress));
if (!mAmbientState.isDark() || mFirstVisibleBackgroundChild != null) {
canvas.drawRoundRect(mBackgroundAnimationRect.left, mBackgroundAnimationRect.top,
mBackgroundAnimationRect.right, mBackgroundAnimationRect.bottom,
@@ -585,14 +581,15 @@ public class NotificationStackScrollLayout extends ViewGroup
float alpha =
BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
alpha *= 1f - mDarkAmount;
alpha *= 1f - mInterpolatedDarkAmount;
// We need to manually blend in the background color.
int scrimColor = mScrimController.getBackgroundColor();
int awakeColor = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
// Interpolate between semi-transparent notification panel background color
// and white AOD separator.
float colorInterpolation = Interpolators.DECELERATE_QUINT.getInterpolation(mDarkAmount);
float colorInterpolation = Interpolators.DECELERATE_QUINT.getInterpolation(
mInterpolatedDarkAmount);
int color = ColorUtils.blendARGB(awakeColor, Color.WHITE, colorInterpolation);
if (mCachedBackgroundColor != color) {
@@ -736,7 +733,8 @@ public class NotificationStackScrollLayout extends ViewGroup
}
private void updateAlgorithmHeightAndPadding() {
mTopPadding = (int) MathUtils.lerp(mRegularTopPadding, mDarkTopPadding, mDarkAmount);
mTopPadding = (int) MathUtils.lerp(mRegularTopPadding, mDarkTopPadding,
mInterpolatedDarkAmount);
mAmbientState.setLayoutHeight(getLayoutHeight());
updateAlgorithmLayoutMinHeight();
mAmbientState.setTopPadding(mTopPadding);
@@ -961,7 +959,7 @@ public class NotificationStackScrollLayout extends ViewGroup
}
public void updateClipping() {
boolean animatingClipping = mDarkAmount > 0 && mDarkAmount < 1;
boolean animatingClipping = mInterpolatedDarkAmount > 0 && mInterpolatedDarkAmount < 1;
boolean clipped = mRequestedClipBounds != null && !mInHeadsUpPinnedMode
&& !mHeadsUpAnimatingAway;
if (mIsClipped != clipped) {
@@ -2421,7 +2419,7 @@ public class NotificationStackScrollLayout extends ViewGroup
return;
}
final boolean awake = mDarkAmount != 0 || mAmbientState.isDark();
final boolean awake = mInterpolatedDarkAmount != 0 || mAmbientState.isDark();
mScrimController.setExcludedBackgroundArea(
mFadingOut || mParentNotFullyVisible || awake || mIsClipped ? null
: mCurrentBounds);
@@ -3414,7 +3412,6 @@ public class NotificationStackScrollLayout extends ViewGroup
.animateY(mShelf));
ev.darkAnimationOriginIndex = mDarkAnimationOriginIndex;
mAnimationEvents.add(ev);
startDarkAmountAnimation();
}
mDarkNeedsAnimation = false;
}
@@ -3990,11 +3987,8 @@ public class NotificationStackScrollLayout extends ViewGroup
if (animate && mAnimationsEnabled) {
mDarkNeedsAnimation = true;
mDarkAnimationOriginIndex = findDarkAnimationOriginIndex(touchWakeUpScreenLocation);
mNeedsAnimation = true;
mNeedsAnimation = true;
} else {
if (mDarkAmountAnimator != null) {
mDarkAmountAnimator.cancel();
}
setDarkAmount(dark ? 1f : 0f);
updateBackground();
}
@@ -4005,7 +3999,7 @@ public class NotificationStackScrollLayout extends ViewGroup
}
private void updatePanelTranslation() {
setTranslationX(mVerticalPanelTranslation + mAntiBurnInOffsetX * mDarkAmount);
setTranslationX(mVerticalPanelTranslation + mAntiBurnInOffsetX * mInterpolatedDarkAmount);
}
public void setVerticalPanelTranslation(float verticalPanelTranslation) {
@@ -4024,9 +4018,22 @@ public class NotificationStackScrollLayout extends ViewGroup
}
private void setDarkAmount(float darkAmount) {
mDarkAmount = darkAmount;
setDarkAmount(darkAmount, darkAmount);
}
/**
* Sets the current dark amount.
*
* @param linearDarkAmount The dark amount that follows linear interpoloation in the animation,
* i.e. animates from 0 to 1 or vice-versa in a linear manner.
* @param interpolatedDarkAmount The dark amount that follows the actual interpolation of the
* animation curve.
*/
public void setDarkAmount(float linearDarkAmount, float interpolatedDarkAmount) {
mLinearDarkAmount = linearDarkAmount;
mInterpolatedDarkAmount = interpolatedDarkAmount;
boolean wasFullyDark = mAmbientState.isFullyDark();
mAmbientState.setDarkAmount(darkAmount);
mAmbientState.setDarkAmount(interpolatedDarkAmount);
boolean nowFullyDark = mAmbientState.isFullyDark();
if (nowFullyDark != wasFullyDark) {
updateContentHeight();
@@ -4044,42 +4051,24 @@ public class NotificationStackScrollLayout extends ViewGroup
requestChildrenUpdate();
}
public float getDarkAmount() {
return mDarkAmount;
public void notifyDarkAnimationStart(boolean dark) {
// We only swap the scaling factor if we're fully dark or fully awake to avoid
// interpolation issues when playing with the power button.
if (mInterpolatedDarkAmount == 0 || mInterpolatedDarkAmount == 1) {
mBackgroundXFactor = dark ? 1.8f : 1.5f;
mDarkXInterpolator = dark
? Interpolators.FAST_OUT_SLOW_IN_REVERSE
: Interpolators.FAST_OUT_SLOW_IN;
}
}
/**
* Cancel any previous dark animations - to avoid race conditions - and creates a new one.
* This function also sets {@code mBackgroundXFactor} based on the current {@code mDarkAmount}.
*/
private void startDarkAmountAnimation() {
boolean dark = mAmbientState.isDark();
if (mDarkAmountAnimator != null) {
mDarkAmountAnimator.cancel();
}
public long getDarkAnimationDuration(boolean dark) {
long duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP;
// Longer animation when sleeping with more than 1 notification
if (dark && getNotGoneChildCount() > 2) {
duration *= 1.2f;
}
mDarkAmountAnimator = ObjectAnimator.ofFloat(this, DARK_AMOUNT, mDarkAmount,
dark ? 1f : 0);
// We only swap the scaling factor if we're fully dark or fully awake to avoid
// interpolation issues when playing with the power button.
if (mDarkAmount == 0 || mDarkAmount == 1) {
mBackgroundXFactor = dark ? 2.5f : 1.5f;
}
mDarkAmountAnimator.setDuration(duration);
mDarkAmountAnimator.setInterpolator(Interpolators.LINEAR);
mDarkAmountAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mDarkAmountAnimator = null;
}
});
mDarkAmountAnimator.start();
return duration;
}
private int findDarkAnimationOriginIndex(@Nullable PointF screenLocation) {