Fix dot for smart reply and bubble groups [DO NOT MERGE]

[Cherrypicked from master]

Android Messages sends two notifications per message
- New notif
- Smart reply update

This change fixes
- dot bugs that occur when two notifications arrive in quick succession for the same bubble
- regressions for bubble groups

BubbleStackView
- clearFlyoutOnHide: enforce flyout onHide to run once for the bubble it was updated for
- refactors for clarity

BubbleView
- shouldShowDot: refactor show-dot logic into function
	updateViews did not account for mSuppressDot
	=> dot flashed into view before being animated away by later dot visibility updates
- updateDotVisibility: add missing call to setDotScale if animate=false

Fixes: 138659213
Test: add bubble group with test app
	expand bubbles => update dots show for non-expanded bubbles
	expand bubbles, click through bubbles => dots go away
	expand bubbles, dismiss single bubble => summary (with one less notif) stays in scrim
Test: add mixed bubble group with test app
	expand bubbles, dismiss all bubbles => scrim notif for non-bubbling group stays

Bug: 138755533
Test: send android messages sms => flyout and dot behave as expected
Test: create bubble with test app => flyout and dot behave as expected
Test: atest SystemUITests
Change-Id: Ieb2e6c306a0b55aec248bd1582246b67eafab290
(cherry picked from commit f1f2c33f2c)
This commit is contained in:
Lyn Han
2019-08-23 17:06:56 -07:00
parent 39195ba211
commit 58a5522283
2 changed files with 101 additions and 104 deletions

View File

@@ -169,7 +169,7 @@ public class BubbleStackView extends FrameLayout {
* Callback to run after the flyout hides. Also called if a new flyout is shown before the * Callback to run after the flyout hides. Also called if a new flyout is shown before the
* previous one animates out. * previous one animates out.
*/ */
private Runnable mAfterFlyoutHides; private Runnable mFlyoutOnHide;
/** Layout change listener that moves the stack to the nearest valid position on rotation. */ /** Layout change listener that moves the stack to the nearest valid position on rotation. */
private OnLayoutChangeListener mOrientationChangedListener; private OnLayoutChangeListener mOrientationChangedListener;
@@ -1401,111 +1401,106 @@ public class BubbleStackView extends FrameLayout {
@VisibleForTesting @VisibleForTesting
void animateInFlyoutForBubble(Bubble bubble) { void animateInFlyoutForBubble(Bubble bubble) {
final CharSequence updateMessage = bubble.getUpdateMessage(getContext()); final CharSequence updateMessage = bubble.getUpdateMessage(getContext());
if (!bubble.showFlyoutForBubble()) { if (!bubble.showFlyoutForBubble()) {
// In case flyout was suppressed for this update, reset now. // In case flyout was suppressed for this update, reset now.
bubble.setSuppressFlyout(false); bubble.setSuppressFlyout(false);
return; return;
} }
if (updateMessage == null if (updateMessage == null
|| isExpanded() || isExpanded()
|| mIsExpansionAnimating || mIsExpansionAnimating
|| mIsGestureInProgress || mIsGestureInProgress
|| mBubbleToExpandAfterFlyoutCollapse != null) { || mBubbleToExpandAfterFlyoutCollapse != null
|| bubble.getIconView() == null) {
// Skip the message if none exists, we're expanded or animating expansion, or we're // Skip the message if none exists, we're expanded or animating expansion, or we're
// about to expand a bubble from the previous tapped flyout. // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
return; return;
} }
mFlyoutDragDeltaX = 0f;
if (bubble.getIconView() != null) { clearFlyoutOnHide();
// Temporarily suppress the dot while the flyout is visible. mFlyoutOnHide = () -> {
bubble.getIconView().setSuppressDot( resetDot(bubble);
true /* suppressDot */, false /* animate */); if (mBubbleToExpandAfterFlyoutCollapse == null) {
return;
mFlyout.removeCallbacks(mAnimateInFlyout);
mFlyoutDragDeltaX = 0f;
if (mAfterFlyoutHides != null) {
mAfterFlyoutHides.run();
} }
mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
mBubbleData.setExpanded(true);
mBubbleToExpandAfterFlyoutCollapse = null;
};
mFlyout.setVisibility(INVISIBLE);
mAfterFlyoutHides = () -> { // Temporarily suppress the dot while the flyout is visible.
final boolean suppressDot = !bubble.showBubbleDot(); bubble.getIconView().setSuppressDot(
// If we're going to suppress the dot, make it visible first so it'll true /* suppressDot */, false /* animate */);
// visibly animate away.
if (suppressDot) {
bubble.getIconView().setSuppressDot(
false /* suppressDot */, false /* animate */);
}
// Reset dot suppression. If we're not suppressing due to DND, then
// stop suppressing it with no animation (since the flyout has
// transformed into the dot). If we are suppressing due to DND, animate
// it away.
bubble.getIconView().setSuppressDot(
suppressDot /* suppressDot */,
suppressDot /* animate */);
if (mBubbleToExpandAfterFlyoutCollapse != null) { // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse); post(() -> {
mBubbleData.setExpanded(true); // An auto-expanding bubble could have been posted during the time it takes to
mBubbleToExpandAfterFlyoutCollapse = null; // layout.
} if (isExpanded()) {
}; return;
}
mFlyout.setVisibility(INVISIBLE); final Runnable expandFlyoutAfterDelay = () -> {
mAnimateInFlyout = () -> {
// Post in case layout isn't complete and getWidth returns 0. mFlyout.setVisibility(VISIBLE);
post(() -> { mFlyoutDragDeltaX =
// An auto-expanding bubble could have been posted during the time it takes to mStackAnimationController.isStackOnLeftSide()
// layout. ? -mFlyout.getWidth()
if (isExpanded()) { : mFlyout.getWidth();
return; animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
} mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
final Runnable afterShow = () -> {
mAnimateInFlyout = () -> {
mFlyout.setVisibility(VISIBLE);
bubble.getIconView().setSuppressDot(
true /* suppressDot */, false /* animate */);
mFlyoutDragDeltaX =
mStackAnimationController.isStackOnLeftSide()
? -mFlyout.getWidth()
: mFlyout.getWidth();
animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
};
mFlyout.postDelayed(mAnimateInFlyout, 200);
}; };
mFlyout.postDelayed(mAnimateInFlyout, 200);
mFlyout.setupFlyoutStartingAsDot( };
updateMessage, mStackAnimationController.getStackPosition(), getWidth(), mFlyout.setupFlyoutStartingAsDot(
mStackAnimationController.isStackOnLeftSide(), updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
bubble.getIconView().getBadgeColor(), mStackAnimationController.isStackOnLeftSide(),
afterShow, bubble.getIconView().getBadgeColor() /* dotColor */,
mAfterFlyoutHides, expandFlyoutAfterDelay /* onLayoutComplete */,
bubble.getIconView().getDotCenter()); mFlyoutOnHide,
mFlyout.bringToFront(); bubble.getIconView().getDotCenter());
}); mFlyout.bringToFront();
} });
mFlyout.removeCallbacks(mHideFlyout); mFlyout.removeCallbacks(mHideFlyout);
mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
} }
private void resetDot(Bubble bubble) {
final boolean suppressDot = !bubble.showBubbleDot();
// If we're going to suppress the dot, make it visible first so it'll
// visibly animate away.
if (suppressDot) {
bubble.getIconView().setSuppressDot(
false /* suppressDot */, false /* animate */);
}
// Reset dot suppression. If we're not suppressing due to DND, then
// stop suppressing it with no animation (since the flyout has
// transformed into the dot). If we are suppressing due to DND, animate
// it away.
bubble.getIconView().setSuppressDot(
suppressDot /* suppressDot */,
suppressDot /* animate */);
}
/** Hide the flyout immediately and cancel any pending hide runnables. */ /** Hide the flyout immediately and cancel any pending hide runnables. */
private void hideFlyoutImmediate() { private void hideFlyoutImmediate() {
if (mAfterFlyoutHides != null) { clearFlyoutOnHide();
mAfterFlyoutHides.run();
}
mFlyout.removeCallbacks(mAnimateInFlyout); mFlyout.removeCallbacks(mAnimateInFlyout);
mFlyout.removeCallbacks(mHideFlyout); mFlyout.removeCallbacks(mHideFlyout);
mFlyout.hideFlyout(); mFlyout.hideFlyout();
} }
private void clearFlyoutOnHide() {
mFlyout.removeCallbacks(mAnimateInFlyout);
if (mFlyoutOnHide == null) {
return;
}
mFlyoutOnHide.run();
mFlyoutOnHide = null;
}
@Override @Override
public void getBoundsOnScreen(Rect outRect) { public void getBoundsOnScreen(Rect outRect) {
if (!mIsExpanded) { if (!mIsExpanded) {

View File

@@ -61,7 +61,7 @@ public class BubbleView extends FrameLayout {
// mBubbleIconFactory cannot be static because it depends on Context. // mBubbleIconFactory cannot be static because it depends on Context.
private BubbleIconFactory mBubbleIconFactory; private BubbleIconFactory mBubbleIconFactory;
private boolean mSuppressDot = false; private boolean mSuppressDot;
private Bubble mBubble; private Bubble mBubble;
@@ -140,6 +140,7 @@ public class BubbleView extends FrameLayout {
public void setAppIcon(Drawable appIcon) { public void setAppIcon(Drawable appIcon) {
mUserBadgedAppIcon = appIcon; mUserBadgedAppIcon = appIcon;
} }
/** /**
* @return the {@link ExpandableNotificationRow} view to display notification content when the * @return the {@link ExpandableNotificationRow} view to display notification content when the
* bubble is expanded. * bubble is expanded.
@@ -154,7 +155,6 @@ public class BubbleView extends FrameLayout {
updateDotVisibility(animate, null /* after */); updateDotVisibility(animate, null /* after */);
} }
/** /**
* Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
* flyout is visible or animating, to hide the dot until the flyout visually transforms into it. * flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
@@ -166,7 +166,7 @@ public class BubbleView extends FrameLayout {
/** Sets the position of the 'new' dot, animating it out and back in if requested. */ /** Sets the position of the 'new' dot, animating it out and back in if requested. */
void setDotPosition(boolean onLeft, boolean animate) { void setDotPosition(boolean onLeft, boolean animate) {
if (animate && onLeft != mBadgedImageView.getDotOnLeft() && !mSuppressDot) { if (animate && onLeft != mBadgedImageView.getDotOnLeft() && shouldShowDot()) {
animateDot(false /* showDot */, () -> { animateDot(false /* showDot */, () -> {
mBadgedImageView.setDotOnLeft(onLeft); mBadgedImageView.setDotOnLeft(onLeft);
animateDot(true /* showDot */, null); animateDot(true /* showDot */, null);
@@ -190,12 +190,12 @@ public class BubbleView extends FrameLayout {
* after animation if requested. * after animation if requested.
*/ */
private void updateDotVisibility(boolean animate, Runnable after) { private void updateDotVisibility(boolean animate, Runnable after) {
boolean showDot = mBubble.showBubbleDot() && !mSuppressDot; final boolean showDot = shouldShowDot();
if (animate) { if (animate) {
animateDot(showDot, after); animateDot(showDot, after);
} else { } else {
mBadgedImageView.setShowDot(showDot); mBadgedImageView.setShowDot(showDot);
mBadgedImageView.setDotScale(showDot ? 1f : 0f);
} }
} }
@@ -203,27 +203,25 @@ public class BubbleView extends FrameLayout {
* Animates the badge to show or hide. * Animates the badge to show or hide.
*/ */
private void animateDot(boolean showDot, Runnable after) { private void animateDot(boolean showDot, Runnable after) {
if (mBadgedImageView.isShowingDot() != showDot) { if (mBadgedImageView.isShowingDot() == showDot) {
if (showDot) { return;
mBadgedImageView.setShowDot(true);
}
mBadgedImageView.clearAnimation();
mBadgedImageView.animate().setDuration(200)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.setUpdateListener((valueAnimator) -> {
float fraction = valueAnimator.getAnimatedFraction();
fraction = showDot ? fraction : 1f - fraction;
mBadgedImageView.setDotScale(fraction);
}).withEndAction(() -> {
if (!showDot) {
mBadgedImageView.setShowDot(false);
}
if (after != null) {
after.run();
}
}).start();
} }
// Do NOT wait until after animation ends to setShowDot
// to avoid overriding more recent showDot states.
mBadgedImageView.setShowDot(showDot);
mBadgedImageView.clearAnimation();
mBadgedImageView.animate().setDuration(200)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.setUpdateListener((valueAnimator) -> {
float fraction = valueAnimator.getAnimatedFraction();
fraction = showDot ? fraction : 1f - fraction;
mBadgedImageView.setDotScale(fraction);
}).withEndAction(() -> {
mBadgedImageView.setDotScale(showDot ? 1f : 0f);
if (after != null) {
after.run();
}
}).start();
} }
void updateViews() { void updateViews() {
@@ -273,7 +271,11 @@ public class BubbleView extends FrameLayout {
iconPath.transform(matrix); iconPath.transform(matrix);
mBadgedImageView.drawDot(iconPath); mBadgedImageView.drawDot(iconPath);
animateDot(mBubble.showBubbleDot() /* showDot */, null /* after */); animateDot(shouldShowDot(), null /* after */);
}
boolean shouldShowDot() {
return mBubble.showBubbleDot() && !mSuppressDot;
} }
int getBadgeColor() { int getBadgeColor() {