Fixed an issue where the notification clipping flickered

Because stopped clipping all views, the top views that were
scrolled could flicker on the screen during notification
launches

Fixes: 191470773
Test: add scrolled notification, launch, observe no flickering
Change-Id: Ia30abfdc6a46e8680bc336c3682ff5e35b0fb606
This commit is contained in:
Selim Cinek
2021-06-18 16:22:12 +02:00
parent a1b3ce5250
commit fb5e240730
8 changed files with 236 additions and 34 deletions

View File

@@ -22,11 +22,27 @@ class ExpandAnimationParameters(
)
var startTranslationZ = 0f
/**
* The top position of the notification at the start of the animation. This is needed in order
* to keep the notification at its place when launching a notification that is clipped rounded.
*/
var startNotificationTop = 0f
var startClipTopAmount = 0
var parentStartClipTopAmount = 0
var progress = 0f
var linearProgress = 0f
/**
* The rounded top clipping at the beginning.
*/
var startRoundedTopClipping = 0
/**
* The rounded top clipping of the parent notification at the start.
*/
var parentStartRoundedTopClipping = 0
override val topChange: Int
get() {
// We need this compensation to ensure that the QS moves in sync.

View File

@@ -40,6 +40,11 @@ class NotificationLaunchAnimatorController(
private val headsUpManager: HeadsUpManagerPhone,
private val notification: ExpandableNotificationRow
) : ActivityLaunchAnimator.Controller {
companion object {
const val ANIMATION_DURATION_TOP_ROUNDING = 100L
}
private val notificationEntry = notification.entry
private val notificationKey = notificationEntry.sbn.key
@@ -54,18 +59,37 @@ class NotificationLaunchAnimatorController(
val height = max(0, notification.actualHeight - notification.clipBottomAmount)
val location = notification.locationOnScreen
val clipStartLocation = notificationListContainer.getTopClippingStartLocation()
val roundedTopClipping = Math.max(clipStartLocation - location[1], 0)
val windowTop = location[1] + roundedTopClipping
val topCornerRadius = if (roundedTopClipping > 0) {
// Because the rounded Rect clipping is complex, we start the top rounding at
// 0, which is pretty close to matching the real clipping.
// We'd have to clipOut the overlaid drawable too with the outer rounded rect in case
// if we'd like to have this perfect, but this is close enough.
0f
} else {
notification.currentBackgroundRadiusTop
}
val params = ExpandAnimationParameters(
top = location[1],
top = windowTop,
bottom = location[1] + height,
left = location[0],
right = location[0] + notification.width,
topCornerRadius = notification.currentBackgroundRadiusTop,
topCornerRadius = topCornerRadius,
bottomCornerRadius = notification.currentBackgroundRadiusBottom
)
params.startTranslationZ = notification.translationZ
params.startNotificationTop = notification.translationY
params.startRoundedTopClipping = roundedTopClipping
params.startClipTopAmount = notification.clipTopAmount
if (notification.isChildInGroup) {
params.startNotificationTop += notification.notificationParent.translationY
val parentRoundedClip = Math.max(clipStartLocation
- notification.notificationParent.locationOnScreen[1], 0)
params.parentStartRoundedTopClipping = parentRoundedClip
val parentClip = notification.notificationParent.clipTopAmount
params.parentStartClipTopAmount = parentClip

View File

@@ -35,6 +35,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.AnimationDrawable;
@@ -85,6 +86,7 @@ import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
@@ -252,6 +254,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private OnExpandClickListener mOnExpandClickListener;
private View.OnClickListener mOnAppClickListener;
private View.OnClickListener mOnFeedbackClickListener;
private Path mExpandingClipPath;
// Listener will be called when receiving a long click event.
// Use #setLongPressPosition to optionally assign positional data with the long press.
@@ -836,6 +839,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {
if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) {
mNotificationParent.setChildIsExpanding(false);
mNotificationParent.setExpandingClipPath(null);
mNotificationParent.setExtraWidthForClipping(0.0f);
mNotificationParent.setMinimumHeightForClipping(0);
}
@@ -2036,7 +2040,22 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
setTranslationZ(translationZ);
float extraWidthForClipping = params.getWidth() - getWidth();
setExtraWidthForClipping(extraWidthForClipping);
int top = params.getTop();
int top;
if (params.getStartRoundedTopClipping() > 0) {
// If we were clipping initially, let's interpolate from the start position to the
// top. Otherwise, we just take the top directly.
float expandProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
params.getProgress(0,
NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
float startTop = params.getStartNotificationTop();
top = (int) Math.min(MathUtils.lerp(startTop,
params.getTop(), expandProgress),
startTop);
} else {
top = params.getTop();
}
int actualHeight = params.getBottom() - top;
setActualHeight(actualHeight);
int startClipTopAmount = params.getStartClipTopAmount();
int clipTopAmount = (int) MathUtils.lerp(startClipTopAmount, 0, params.getProgress());
if (mNotificationParent != null) {
@@ -2065,13 +2084,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
setClipTopAmount(clipTopAmount);
}
setTranslationY(top);
setActualHeight(params.getHeight());
mTopRoundnessDuringExpandAnimation = params.getTopCornerRadius() / mOutlineRadius;
mBottomRoundnessDuringExpandAnimation = params.getBottomCornerRadius() / mOutlineRadius;
invalidateOutline();
mBackgroundNormal.setExpandAnimationParams(params);
mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight);
}
public void setExpandAnimationRunning(boolean expandAnimationRunning) {
@@ -3085,6 +3103,26 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return super.childNeedsClipping(child);
}
/**
* Set a clip path to be set while expanding the notification. This is needed to nicely
* clip ourselves during the launch if we were clipped rounded in the beginning
*/
public void setExpandingClipPath(Path path) {
mExpandingClipPath = path;
invalidate();
}
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
if (mExpandingClipPath != null && (mExpandAnimationRunning || mChildIsExpanding)) {
// If we're launching a notification, let's clip if a clip rounded to the clipPath
canvas.clipPath(mExpandingClipPath);
}
super.dispatchDraw(canvas);
canvas.restore();
}
@Override
protected void applyRoundness() {
super.applyRoundness();

View File

@@ -240,10 +240,10 @@ public class NotificationBackgroundView extends View {
invalidate();
}
/** Set the current expand animation parameters. */
public void setExpandAnimationParams(ExpandAnimationParameters params) {
mActualHeight = params.getHeight();
mActualWidth = params.getWidth();
/** Set the current expand animation size. */
public void setExpandAnimationSize(int actualWidth, int actualHeight) {
mActualHeight = actualHeight;
mActualWidth = actualWidth;
invalidate();
}

View File

@@ -72,8 +72,6 @@ public class AmbientState {
private boolean mUnlockHintRunning;
private boolean mQsCustomizerShowing;
private int mIntrinsicPadding;
private int mExpandAnimationTopChange;
private ExpandableNotificationRow mExpandingNotification;
private float mHideAmount;
private boolean mAppearing;
private float mPulseHeight = MAX_PULSE_HEIGHT;
@@ -518,22 +516,6 @@ public class AmbientState {
return isDozing() && !isPulsing(row.getEntry());
}
public void setExpandAnimationTopChange(int expandAnimationTopChange) {
mExpandAnimationTopChange = expandAnimationTopChange;
}
public void setExpandingNotification(ExpandableNotificationRow row) {
mExpandingNotification = row;
}
public ExpandableNotificationRow getExpandingNotification() {
return mExpandingNotification;
}
public int getExpandAnimationTopChange() {
return mExpandAnimationTopChange;
}
/**
* @return {@code true } when shade is completely hidden: in AOD, ambient display or when
* bypassing.

View File

@@ -200,4 +200,11 @@ public interface NotificationListContainer extends
default void setWillExpand(boolean willExpand) {}
void setNotificationActivityStarter(NotificationActivityStarter notificationActivityStarter);
/**
* @return the start location where we start clipping notifications.
*/
default int getTopClippingStartLocation() {
return 0;
}
}

View File

@@ -77,6 +77,7 @@ import com.android.settingslib.Utils;
import com.android.systemui.Dumpable;
import com.android.systemui.ExpandHelper;
import com.android.systemui.R;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
import com.android.systemui.statusbar.CommandQueue;
@@ -90,6 +91,7 @@ import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.ShadeViewRefactor;
import com.android.systemui.statusbar.notification.ShadeViewRefactor.RefactorComponent;
@@ -466,6 +468,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
*/
private final Path mRoundedClipPath = new Path();
/**
* The clip Path used to clip the launching notification. This may be different
* from the normal path, as the views launch animation could start clipped.
*/
private final Path mLaunchedNotificationClipPath = new Path();
/**
* Should we use rounded rect clipping right now
*/
@@ -488,6 +496,26 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
*/
private boolean mLaunchingNotification;
/**
* Does the launching notification need to be clipped
*/
private boolean mLaunchingNotificationNeedsToBeClipped;
/**
* The current launch animation params when launching a notification
*/
private ExpandAnimationParameters mLaunchAnimationParams;
/**
* Corner radii of the launched notification if it's clipped
*/
private float[] mLaunchedNotificationRadii = new float[8];
/**
* The notification that is being launched currently.
*/
private ExpandableNotificationRow mExpandingNotificationRow;
/**
* Do notifications dismiss with normal transitioning
*/
@@ -2726,6 +2754,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
* @return the amount of scrolling needed to start clipping notifications.
*/
private int getScrollAmountToScrollBoundary() {
if (mShouldUseSplitNotificationShade) {
return mSidePaddings;
}
return mTopPadding - mQsScrollBoundaryPosition;
}
@@ -2869,7 +2900,16 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
void setExpandingNotification(ExpandableNotificationRow row) {
mAmbientState.setExpandingNotification(row);
if (mExpandingNotificationRow != null && row == null) {
// Let's unset the clip path being set during launch
mExpandingNotificationRow.setExpandingClipPath(null);
ExpandableNotificationRow parent = mExpandingNotificationRow.getNotificationParent();
if (parent != null) {
parent.setExpandingClipPath(null);
}
}
mExpandingNotificationRow = row;
updateLaunchedNotificationClipPath();
requestChildrenUpdate();
}
@@ -2879,10 +2919,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
public void applyExpandAnimationParams(ExpandAnimationParameters params) {
mAmbientState.setExpandAnimationTopChange(params == null ? 0 : params.getTopChange());
// Disable clipping for launches
// Modify the clipping for launching notifications
mLaunchAnimationParams = params;
setLaunchingNotification(params != null);
updateLaunchedNotificationClipPath();
requestChildrenUpdate();
}
@@ -5330,7 +5370,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
return;
}
mLaunchingNotification = launching;
updateUseRoundedRectClipping();
mLaunchingNotificationNeedsToBeClipped = mLaunchAnimationParams != null
&& (mLaunchAnimationParams.getStartRoundedTopClipping() > 0
|| mLaunchAnimationParams.getParentStartRoundedTopClipping() > 0);
if (!mLaunchingNotificationNeedsToBeClipped || !mLaunchingNotification) {
mLaunchedNotificationClipPath.reset();
}
// When launching notifications, we're clipping the children individually instead of in
// dispatchDraw
invalidate();
}
/**
@@ -5340,22 +5388,97 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
// We don't want to clip notifications when QS is expanded, because incoming heads up on
// the bottom would be clipped otherwise
boolean qsAllowsClipping = mQsExpansionFraction < 0.5f || mShouldUseSplitNotificationShade;
boolean clip = !mLaunchingNotification && mIsExpanded && qsAllowsClipping;
boolean clip = mIsExpanded && qsAllowsClipping;
if (clip != mShouldUseRoundedRectClipping) {
mShouldUseRoundedRectClipping = clip;
invalidate();
}
}
/**
* Update the clip path for launched notifications in case they were originally clipped
*/
private void updateLaunchedNotificationClipPath() {
if (!mLaunchingNotificationNeedsToBeClipped || !mLaunchingNotification
|| mExpandingNotificationRow == null) {
return;
}
int left = Math.min(mLaunchAnimationParams.getLeft(), mRoundedRectClippingLeft);
int right = Math.max(mLaunchAnimationParams.getRight(), mRoundedRectClippingRight);
int bottom = Math.max(mLaunchAnimationParams.getBottom(), mRoundedRectClippingBottom);
float expandProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
mLaunchAnimationParams.getProgress(0,
NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
int top = (int) Math.min(MathUtils.lerp(mRoundedRectClippingTop,
mLaunchAnimationParams.getTop(), expandProgress),
mRoundedRectClippingTop);
float topRadius = mLaunchAnimationParams.getTopCornerRadius();
float bottomRadius = mLaunchAnimationParams.getBottomCornerRadius();
mLaunchedNotificationRadii[0] = topRadius;
mLaunchedNotificationRadii[1] = topRadius;
mLaunchedNotificationRadii[2] = topRadius;
mLaunchedNotificationRadii[3] = topRadius;
mLaunchedNotificationRadii[4] = bottomRadius;
mLaunchedNotificationRadii[5] = bottomRadius;
mLaunchedNotificationRadii[6] = bottomRadius;
mLaunchedNotificationRadii[7] = bottomRadius;
mLaunchedNotificationClipPath.reset();
mLaunchedNotificationClipPath.addRoundRect(left, top, right, bottom,
mLaunchedNotificationRadii, Path.Direction.CW);
// Offset into notification clip coordinates instead of parent ones.
// This is needed since the notification changes in translationZ, where clipping via
// canvas dispatching won't work.
ExpandableNotificationRow expandingRow = mExpandingNotificationRow;
if (expandingRow.getNotificationParent() != null) {
expandingRow = expandingRow.getNotificationParent();
}
mLaunchedNotificationClipPath.offset(
-expandingRow.getLeft() - expandingRow.getTranslationX(),
-expandingRow.getTop() - expandingRow.getTranslationY());
expandingRow.setExpandingClipPath(mLaunchedNotificationClipPath);
if (mShouldUseRoundedRectClipping) {
invalidate();
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mShouldUseRoundedRectClipping) {
if (mShouldUseRoundedRectClipping && !mLaunchingNotification) {
// When launching notifications, we're clipping the children individually instead of in
// dispatchDraw
// Let's clip rounded.
canvas.clipPath(mRoundedClipPath);
}
super.dispatchDraw(canvas);
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
if (mShouldUseRoundedRectClipping && mLaunchingNotification) {
// Let's clip children individually during notification launch
canvas.save();
ExpandableView expandableView = (ExpandableView) child;
Path clipPath;
if (expandableView.isExpandAnimationRunning()
|| ((ExpandableView) child).hasExpandingChild()) {
// When launching the notification, it is not clipped by this layout, but by the
// view itself. This is because the view is Translating in Z, where this clipPath
// wouldn't apply.
clipPath = null;
} else {
clipPath = mRoundedClipPath;
}
if (clipPath != null) {
canvas.clipPath(clipPath);
}
boolean result = super.drawChild(canvas, child, drawingTime);
canvas.restore();
return result;
} else {
return super.drawChild(canvas, child, drawingTime);
}
}
/**
* Calculate the total translation needed when dismissing.
*/
@@ -5369,6 +5492,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
return containerWidth - padding;
}
/**
* @return the start location where we start clipping notifications.
*/
public int getTopClippingStartLocation() {
return mIsExpanded ? mQsScrollBoundaryPosition : 0;
}
/**
* A listener that is notified when the empty space below the notifications is clicked on
*/

View File

@@ -1532,6 +1532,11 @@ public class NotificationStackScrollLayoutController {
mView.setNotificationActivityStarter(notificationActivityStarter);
}
@Override
public int getTopClippingStartLocation() {
return mView.getTopClippingStartLocation();
}
@Override
public View getContainerChildAt(int i) {
return mView.getContainerChildAt(i);