From 1812924a53ca6a14a97025d5291ac57b5127d452 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Mon, 10 Jul 2023 13:25:12 -0700 Subject: [PATCH 1/2] Handle any image / label changes for bubble updates in bubble bar When we get an update to a bubble it could mean that there's a new message OR that something about the visual representation changed. This CL modifies BubbleBarController to handle any visual changes that might have occurred to an updated bubble (e.g. bubble image changed). It does this by updating the bubbleInfo on the existing bubble. Test: manual Bug: 269670235 Change-Id: I03d2510aef335dafccb32d6adcd4c6adf8b3297d --- .../taskbar/bubbles/BubbleBarController.java | 40 +++++++++++++------ .../taskbar/bubbles/BubbleBarItem.kt | 18 ++++----- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index 056fc74c30..029c23f872 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -243,17 +243,21 @@ public class BubbleBarController extends IBubblesListener.Stub { BUBBLE_STATE_EXECUTOR.execute(() -> { createAndAddOverflowIfNeeded(); if (update.addedBubble != null) { - viewUpdate.addedBubble = populateBubble(update.addedBubble, mContext, mBarView); + viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView, + null /* existingBubble */); } if (update.updatedBubble != null) { + BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey()); viewUpdate.updatedBubble = - populateBubble(update.updatedBubble, mContext, mBarView); + populateBubble(mContext, update.updatedBubble, mBarView, + existingBubble); } if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) { List currentBubbles = new ArrayList<>(); for (int i = 0; i < update.currentBubbleList.size(); i++) { BubbleBarBubble b = - populateBubble(update.currentBubbleList.get(i), mContext, mBarView); + populateBubble(mContext, update.currentBubbleList.get(i), mBarView, + null /* existingBubble */); currentBubbles.add(b); } viewUpdate.currentBubbles = currentBubbles; @@ -407,7 +411,8 @@ public class BubbleBarController extends IBubblesListener.Stub { // @Nullable - private BubbleBarBubble populateBubble(BubbleInfo b, Context context, BubbleBarView bbv) { + private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv, + @Nullable BubbleBarBubble existingBubble) { String appName; Bitmap badgeBitmap; Bitmap bubbleBitmap; @@ -476,16 +481,27 @@ public class BubbleBarController extends IBubblesListener.Stub { iconPath.transform(matrix); dotPath = iconPath; dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, - Color.WHITE, WHITE_SCRIM_ALPHA); + Color.WHITE, WHITE_SCRIM_ALPHA / 255f); - LayoutInflater inflater = LayoutInflater.from(context); - BubbleView bubbleView = (BubbleView) inflater.inflate( - R.layout.bubblebar_item_view, bbv, false /* attachToRoot */); + if (existingBubble == null) { + LayoutInflater inflater = LayoutInflater.from(context); + BubbleView bubbleView = (BubbleView) inflater.inflate( + R.layout.bubblebar_item_view, bbv, false /* attachToRoot */); - BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView, - badgeBitmap, bubbleBitmap, dotColor, dotPath, appName); - bubbleView.setBubble(bubble); - return bubble; + BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView, + badgeBitmap, bubbleBitmap, dotColor, dotPath, appName); + bubbleView.setBubble(bubble); + return bubble; + } else { + // If we already have a bubble (so it already has an inflated view), update it. + existingBubble.setInfo(b); + existingBubble.setBadge(badgeBitmap); + existingBubble.setIcon(bubbleBitmap); + existingBubble.setDotColor(dotColor); + existingBubble.setDotPath(dotPath); + existingBubble.setAppName(appName); + return existingBubble; + } } private BubbleBarOverflow createOverflow(Context context) { diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt index 582dcc750f..43e21f4085 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt @@ -20,18 +20,18 @@ import android.graphics.Path import com.android.wm.shell.common.bubbles.BubbleInfo /** An entity in the bubble bar. */ -sealed class BubbleBarItem(open val key: String, open val view: BubbleView) +sealed class BubbleBarItem(open var key: String, open var view: BubbleView) /** Contains state info about a bubble in the bubble bar as well as presentation information. */ data class BubbleBarBubble( - val info: BubbleInfo, - override val view: BubbleView, - val badge: Bitmap, - val icon: Bitmap, - val dotColor: Int, - val dotPath: Path, - val appName: String + var info: BubbleInfo, + override var view: BubbleView, + var badge: Bitmap, + var icon: Bitmap, + var dotColor: Int, + var dotPath: Path, + var appName: String ) : BubbleBarItem(info.key, view) /** Represents the overflow bubble in the bubble bar. */ -data class BubbleBarOverflow(override val view: BubbleView) : BubbleBarItem("Overflow", view) +data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view) From c299ad645ac42658b0a404f5940d6f5d2769cd64 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Mon, 10 Jul 2023 13:31:56 -0700 Subject: [PATCH 2/2] Show / hide the "update" dot on bubbles in bubble bar Updates BubbleView to include logic to render the update dot on a bubble. This only shows for BubbleBarBubbles, not the overflow. We only show the dot (and the badge) when: - the bubble has new content / appropriate flags set - AND the bubbles are expanded OR on the first bubble when bubbles are collapsed - AND when the flyout is not animating (this bit doesn't exist yet) If a bubble has a dot and is opened, the dot will animate away. To do this, we update the flags set on a bubble. The flag needs to be set on WMShell side as well as Launcher side. When a bubble is shown by WMShell, it automatically updates the flag. This CL adds code to update the flag on Launcher side when we call into WMShell to show the bubble. Test: manual Bug: 269670235 Change-Id: I32f652effa9a73c567981aa5a2a5864e9c3c0c66 --- .../taskbar/bubbles/BubbleBarController.java | 17 +- .../taskbar/bubbles/BubbleBarView.java | 11 +- .../launcher3/taskbar/bubbles/BubbleView.java | 167 +++++++++++++++--- 3 files changed, 164 insertions(+), 31 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index 029c23f872..7e2b037a3c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -33,6 +33,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_S import android.annotation.BinderThread; import android.annotation.Nullable; +import android.app.Notification; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; @@ -319,9 +320,11 @@ public class BubbleBarController extends IBubblesListener.Stub { mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty()); if (update.updatedBubble != null) { - // TODO: (b/269670235) handle updates: - // (1) if content / icons change -- requires reload & add back in place - // (2) if showing update dot changes -- tell the view to hide / show the dot + // Updates mean the dot state may have changed; any other changes were updated in + // the populateBubble step. + BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey()); + // If we're not stashed, we're visible so animate + bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */); } if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) { // Create the new list @@ -366,7 +369,13 @@ public class BubbleBarController extends IBubblesListener.Stub { if (getSelectedBubbleKey() != null) { int[] bubbleBarCoords = mBarView.getLocationOnScreen(); if (mSelectedBubble instanceof BubbleBarBubble) { - // TODO (b/269670235): hide the update dot on the view if needed. + // Because we've visited this bubble, we should suppress the notification. + // This is updated on WMShell side when we show the bubble, but that update isn't + // passed to launcher, instead we apply it directly here. + BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo(); + info.setFlags( + info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); + mSelectedBubble.getView().updateDotVisibility(true /* animate */); } mSystemUiProxy.showBubble(getSelectedBubbleKey(), bubbleBarCoords[0], bubbleBarCoords[1]); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index 563ba02a42..cf52a5e7e8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -234,6 +234,7 @@ public class BubbleBarView extends FrameLayout { final float collapsedWidth = collapsedWidth(); int bubbleCount = getChildCount(); final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f; + final boolean animate = getVisibility() == VISIBLE; for (int i = 0; i < bubbleCount; i++) { BubbleView bv = (BubbleView) getChildAt(i); bv.setTranslationY(ty); @@ -251,16 +252,14 @@ public class BubbleBarView extends FrameLayout { if (widthState == 1f) { bv.setZ(0); } - bv.showBadge(); + // When we're expanded, we're not stacked so we're not behind the stack + bv.setBehindStack(false, animate); } else { final float targetX = currentWidth - collapsedWidth + collapsedX; bv.setTranslationX(widthState * (expandedX - targetX) + targetX); bv.setZ((MAX_BUBBLES * mBubbleElevation) - i); - if (i > 0) { - bv.hideBadge(); - } else { - bv.showBadge(); - } + // If we're not the first bubble we're behind the stack + bv.setBehindStack(i > 0, animate); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java index 92b76a6f85..12cb8c5353 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java @@ -18,7 +18,9 @@ package com.android.launcher3.taskbar.bubbles; import android.annotation.Nullable; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Outline; +import android.graphics.Rect; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -28,10 +30,13 @@ import android.widget.ImageView; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.R; +import com.android.launcher3.icons.DotRenderer; import com.android.launcher3.icons.IconNormalizer; +import com.android.wm.shell.animation.Interpolators; + +import java.util.EnumSet; // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share. -// TODO: (b/269670235) currently this doesn't show the 'update dot' /** * View that displays a bubble icon, along with an app badge on either the left or @@ -39,14 +44,42 @@ import com.android.launcher3.icons.IconNormalizer; */ public class BubbleView extends ConstraintLayout { - // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that. public static final int DEFAULT_PATH_SIZE = 100; + /** + * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or + * another. If any of these flags are set, the dot will not be shown. + * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown. + */ + enum SuppressionFlag { + // TODO: (b/277815200) implement flyout + // Suppressed because the flyout is visible - it will morph into the dot via animation. + FLYOUT_VISIBLE, + // Suppressed because this bubble is behind others in the collapsed stack. + BEHIND_STACK, + } + + private final EnumSet mSuppressionFlags = + EnumSet.noneOf(SuppressionFlag.class); + private final ImageView mBubbleIcon; private final ImageView mAppIcon; private final int mBubbleSize; + private DotRenderer mDotRenderer; + private DotRenderer.DrawParams mDrawParams; + private int mDotColor; + private Rect mTempBounds = new Rect(); + + // Whether the dot is animating + private boolean mDotIsAnimating; + // What scale value the dot is animating to + private float mAnimatingToDotScale; + // The current scale value of the dot + private float mDotScale; + // TODO: (b/273310265) handle RTL + // Whether the bubbles are positioned on the left or right side of the screen private boolean mOnLeft = false; private BubbleBarItem mBubble; @@ -75,6 +108,8 @@ public class BubbleView extends ConstraintLayout { mBubbleIcon = findViewById(R.id.icon_view); mAppIcon = findViewById(R.id.app_icon_view); + mDrawParams = new DotRenderer.DrawParams(); + setFocusable(true); setClickable(true); setOutlineProvider(new ViewOutlineProvider() { @@ -91,17 +126,43 @@ public class BubbleView extends ConstraintLayout { outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize); } + @Override + public void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (!shouldDrawDot()) { + return; + } + + getDrawingRect(mTempBounds); + + mDrawParams.dotColor = mDotColor; + mDrawParams.iconBounds = mTempBounds; + mDrawParams.leftAlign = mOnLeft; + mDrawParams.scale = mDotScale; + + mDotRenderer.draw(canvas, mDrawParams); + } + /** Sets the bubble being rendered in this view. */ void setBubble(BubbleBarBubble bubble) { mBubble = bubble; mBubbleIcon.setImageBitmap(bubble.getIcon()); mAppIcon.setImageBitmap(bubble.getBadge()); + mDotColor = bubble.getDotColor(); + mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE); } + /** + * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles + * but does not represent app content, instead it shows recent bubbles that couldn't fit into + * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't + * come from an app. + */ void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) { mBubble = overflow; mBubbleIcon.setImageBitmap(bitmap); - hideBadge(); + mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge } /** Returns the bubble being rendered in this view. */ @@ -110,38 +171,102 @@ public class BubbleView extends ConstraintLayout { return mBubble; } - /** Shows the app badge on this bubble. */ - void showBadge() { + void updateDotVisibility(boolean animate) { + final float targetScale = shouldDrawDot() ? 1f : 0f; + if (animate) { + animateDotScale(); + } else { + mDotScale = targetScale; + mAnimatingToDotScale = targetScale; + invalidate(); + } + } + + void updateBadgeVisibility() { if (mBubble instanceof BubbleBarOverflow) { // The overflow bubble does not have a badge, so just bail. return; } BubbleBarBubble bubble = (BubbleBarBubble) mBubble; - Bitmap appBadgeBitmap = bubble.getBadge(); - if (appBadgeBitmap == null) { - mAppIcon.setVisibility(GONE); + int translationX = mOnLeft + ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth()) + : 0; + mAppIcon.setTranslationX(translationX); + mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE); + } + + /** Sets whether this bubble is in the stack & not the first bubble. **/ + void setBehindStack(boolean behindStack, boolean animate) { + if (behindStack) { + mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK); + } else { + mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK); + } + updateDotVisibility(animate); + updateBadgeVisibility(); + } + + /** Whether this bubble is in the stack & not the first bubble. **/ + boolean isBehindStack() { + return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK); + } + + /** Whether the dot indicating unseen content in a bubble should be shown. */ + private boolean shouldDrawDot() { + boolean bubbleHasUnseenContent = mBubble != null + && mBubble instanceof BubbleBarBubble + && mSuppressionFlags.isEmpty() + && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed(); + + // Always render the dot if it's animating, since it could be animating out. Otherwise, show + // it if the bubble wants to show it, and we aren't suppressing it. + return bubbleHasUnseenContent || mDotIsAnimating; + } + + /** How big the dot should be, fraction from 0 to 1. */ + private void setDotScale(float fraction) { + mDotScale = fraction; + invalidate(); + } + + /** + * Animates the dot to the given scale. + */ + private void animateDotScale() { + float toScale = shouldDrawDot() ? 1f : 0f; + mDotIsAnimating = true; + + // Don't restart the animation if we're already animating to the given value. + if (mAnimatingToDotScale == toScale || !shouldDrawDot()) { + mDotIsAnimating = false; return; } - int translationX; - if (mOnLeft) { - translationX = -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth()); - } else { - translationX = 0; - } + mAnimatingToDotScale = toScale; - mAppIcon.setTranslationX(translationX); - mAppIcon.setVisibility(VISIBLE); + final boolean showDot = toScale > 0f; + + // Do NOT wait until after animation ends to setShowDot + // to avoid overriding more recent showDot states. + clearAnimation(); + animate() + .setDuration(200) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setUpdateListener((valueAnimator) -> { + float fraction = valueAnimator.getAnimatedFraction(); + fraction = showDot ? fraction : 1f - fraction; + setDotScale(fraction); + }).withEndAction(() -> { + setDotScale(showDot ? 1f : 0f); + mDotIsAnimating = false; + }).start(); } - /** Hides the app badge on this bubble. */ - void hideBadge() { - mAppIcon.setVisibility(GONE); - } @Override public String toString() { - return "BubbleView{" + mBubble + "}"; + String toString = mBubble != null ? mBubble.getKey() : "null"; + return "BubbleView{" + toString + "}"; } }