Assorted dot-wrangling.
- Fixes issue with dots showing up on the wrong side, because code assumed the stack starts on the right when it actually starts on the left now.
- Animates dots out when they're behind the stack/when collapsing from expanded state.
- Animates dots out when expanding.
Remaining issues include: hiding the app badge (deceptively hard), updating state for DND as soon as it changes (vs. when a new bubble is posted, also hard).
Test: manual
Bug: 145245204
Bug: 137213469
Change-Id: I9e2ff29c62ba8c8d0f052e42386b5d517952984e
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
index a1cb7f6..0b59ebc 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
@@ -28,6 +28,8 @@
import com.android.systemui.Interpolators;
import com.android.systemui.R;
+import java.util.EnumSet;
+
/**
* View that displays an adaptive icon with an app-badge and a dot.
*
@@ -42,12 +44,27 @@
/** Same as value in Launcher3 IconShape */
public static final int DEFAULT_PATH_SIZE = 100;
- static final int DOT_STATE_DEFAULT = 0;
- static final int DOT_STATE_SUPPRESSED_FOR_FLYOUT = 1;
- static final int DOT_STATE_ANIMATING = 2;
+ /**
+ * Flags that suppress the visibility of the 'new' dot, for one reason or another. If any of
+ * these flags are set, the dot will not be shown even if {@link Bubble#showDot()} returns true.
+ */
+ enum SuppressionFlag {
+ // 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,
+ }
- // Flyout gets shown before the dot
- private int mCurrentDotState = DOT_STATE_SUPPRESSED_FOR_FLYOUT;
+ /**
+ * Start by suppressing the dot because the flyout is visible - most bubbles are added with a
+ * flyout, so this is a reasonable default.
+ */
+ private final EnumSet<SuppressionFlag> mDotSuppressionFlags =
+ EnumSet.of(SuppressionFlag.FLYOUT_VISIBLE);
+
+ private float mDotScale = 0f;
+ private float mAnimatingToDotScale = 0f;
+ private boolean mDotIsAnimating = false;
private BubbleViewProvider mBubble;
@@ -57,8 +74,6 @@
private boolean mOnLeft;
private int mDotColor;
- private float mDotScale = 0f;
- private boolean mDotDrawn;
private Rect mTempBounds = new Rect();
@@ -88,23 +103,21 @@
/**
* Updates the view with provided info.
*/
- public void update(BubbleViewProvider bubble) {
+ public void setRenderedBubble(BubbleViewProvider bubble) {
mBubble = bubble;
setImageBitmap(bubble.getBadgedImage());
- setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
mDotColor = bubble.getDotColor();
drawDot(bubble.getDotPath());
- animateDot();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
- if (isDotHidden()) {
- mDotDrawn = false;
+
+ if (!shouldDrawDot()) {
return;
}
- mDotDrawn = mDotScale > 0.1f;
+
getDrawingRect(mTempBounds);
mDrawParams.color = mDotColor;
@@ -115,23 +128,33 @@
mDotRenderer.draw(canvas, mDrawParams);
}
- /**
- * Sets the dot state, does not animate changes.
- */
- void setDotState(int state) {
- mCurrentDotState = state;
- if (state == DOT_STATE_SUPPRESSED_FOR_FLYOUT || state == DOT_STATE_DEFAULT) {
- mDotScale = mBubble.showDot() ? 1f : 0f;
- invalidate();
+ /** Adds a dot suppression flag, updating dot visibility if needed. */
+ void addDotSuppressionFlag(SuppressionFlag flag) {
+ if (mDotSuppressionFlags.add(flag)) {
+ // Update dot visibility, and animate out if we're now behind the stack.
+ updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK /* animate */);
}
}
- /**
- * Whether the dot should be hidden based on current dot state.
- */
- private boolean isDotHidden() {
- return (mCurrentDotState == DOT_STATE_DEFAULT && !mBubble.showDot())
- || mCurrentDotState == DOT_STATE_SUPPRESSED_FOR_FLYOUT;
+ /** Removes a dot suppression flag, updating dot visibility if needed. */
+ void removeDotSuppressionFlag(SuppressionFlag flag) {
+ if (mDotSuppressionFlags.remove(flag)) {
+ // Update dot visibility, animating if we're no longer behind the stack.
+ updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK);
+ }
+ }
+
+ /** Updates the visibility of the dot, animating if requested. */
+ void updateDotVisibility(boolean animate) {
+ final float targetScale = shouldDrawDot() ? 1f : 0f;
+
+ if (animate) {
+ animateDotScale(targetScale, null /* after */);
+ } else {
+ mDotScale = targetScale;
+ mAnimatingToDotScale = targetScale;
+ invalidate();
+ }
}
/**
@@ -194,11 +217,11 @@
}
/** Sets the position of the 'new' dot, animating it out and back in if requested. */
- void setDotPosition(boolean onLeft, boolean animate) {
- if (animate && onLeft != getDotOnLeft() && !isDotHidden()) {
- animateDot(false /* showDot */, () -> {
+ void setDotPositionOnLeft(boolean onLeft, boolean animate) {
+ if (animate && onLeft != getDotOnLeft() && shouldDrawDot()) {
+ animateDotScale(0f /* showDot */, () -> {
setDotOnLeft(onLeft);
- animateDot(true /* showDot */, null);
+ animateDotScale(1.0f, null /* after */);
});
} else {
setDotOnLeft(onLeft);
@@ -209,28 +232,34 @@
return getDotOnLeft();
}
- /** Changes the dot's visibility to match the bubble view's state. */
- void animateDot() {
- if (mCurrentDotState == DOT_STATE_DEFAULT) {
- animateDot(mBubble.showDot(), null);
- }
+ /** Whether to draw the dot in onDraw(). */
+ private boolean shouldDrawDot() {
+ // 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 mDotIsAnimating || (mBubble.showDot() && mDotSuppressionFlags.isEmpty());
}
/**
- * Animates the dot to show or hide.
+ * Animates the dot to the given scale, running the optional callback when the animation ends.
*/
- private void animateDot(boolean showDot, Runnable after) {
- if (mDotDrawn == showDot) {
- // State is consistent, do nothing.
+ private void animateDotScale(float toScale, @Nullable Runnable after) {
+ mDotIsAnimating = true;
+
+ // Don't restart the animation if we're already animating to the given value.
+ if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
+ mDotIsAnimating = false;
return;
}
- setDotState(DOT_STATE_ANIMATING);
+ mAnimatingToDotScale = toScale;
+
+ 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)
+ animate()
+ .setDuration(200)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.setUpdateListener((valueAnimator) -> {
float fraction = valueAnimator.getAnimatedFraction();
@@ -238,7 +267,7 @@
setDotScale(fraction);
}).withEndAction(() -> {
setDotScale(showDot ? 1f : 0f);
- setDotState(DOT_STATE_DEFAULT);
+ mDotIsAnimating = false;
if (after != null) {
after.run();
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 726a7dd..afa3164 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -247,7 +247,7 @@
mExpandedView.update(/* bubble */ this);
}
if (mIconView != null) {
- mIconView.update(/* bubble */ this);
+ mIconView.setRenderedBubble(/* bubble */ this);
}
}
@@ -306,7 +306,7 @@
void markAsAccessedAt(long lastAccessedMillis) {
mLastAccessed = lastAccessedMillis;
setSuppressNotification(true);
- setShowDot(false /* show */, true /* animate */);
+ setShowDot(false /* show */);
}
/**
@@ -346,12 +346,11 @@
/**
* Sets whether the bubble for this notification should show a dot indicating updated content.
*/
- void setShowDot(boolean showDot, boolean animate) {
+ void setShowDot(boolean showDot) {
mShowBubbleUpdateDot = showDot;
- if (animate && mIconView != null) {
- mIconView.animateDot();
- } else if (mIconView != null) {
- mIconView.invalidate();
+
+ if (mIconView != null) {
+ mIconView.updateDotVisibility(true /* animate */);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 01c2faa..9d885fd 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -331,14 +331,14 @@
@Override
public void onZenChanged(int zen) {
for (Bubble b : mBubbleData.getBubbles()) {
- b.setShowDot(b.showInShade(), true /* animate */);
+ b.setShowDot(b.showInShade());
}
}
@Override
public void onConfigChanged(ZenModeConfig config) {
for (Bubble b : mBubbleData.getBubbles()) {
- b.setShowDot(b.showInShade(), true /* animate */);
+ b.setShowDot(b.showInShade());
}
}
});
@@ -1101,7 +1101,7 @@
} else if (interceptBubbleDismissal) {
Bubble bubble = mBubbleData.getBubbleWithKey(entry.getKey());
bubble.setSuppressNotification(true);
- bubble.setShowDot(false /* show */, true /* animate */);
+ bubble.setShowDot(false /* show */);
} else {
return false;
}
@@ -1141,7 +1141,7 @@
Bubble bubbleChild = mBubbleData.getBubbleWithKey(child.getKey());
mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry());
bubbleChild.setSuppressNotification(true);
- bubbleChild.setShowDot(false /* show */, true /* animate */);
+ bubbleChild.setShowDot(false /* show */);
} else {
// non-bubbled children can be removed
for (NotifCallback cb : mCallbacks) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 2bd1518..1c69594 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -288,7 +288,7 @@
boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
bubble.setSuppressNotification(suppress);
- bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
+ bubble.setShowDot(!isBubbleExpandedAndSelected /* show */);
dispatchPendingChanges();
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java
index 4fb2d08..13669a6 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java
@@ -112,7 +112,7 @@
mPath.transform(matrix);
mOverflowBtn.setVisibility(GONE);
- mOverflowBtn.update(this);
+ mOverflowBtn.setRenderedBubble(this);
}
ImageView getBtn() {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
index b651985..2231d11 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
@@ -183,7 +183,7 @@
public void onBindViewHolder(ViewHolder vh, int index) {
Bubble b = mBubbles.get(index);
- vh.iconView.update(b);
+ vh.iconView.setRenderedBubble(b);
vh.iconView.setOnClickListener(view -> {
mBubbles.remove(b);
notifyDataSetChanged();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 7191a20..4b03681 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -22,8 +22,6 @@
import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION;
import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION;
-import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT;
-import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT;
import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
@@ -226,7 +224,7 @@
private boolean mIsExpanded;
/** Whether the stack is currently on the left side of the screen, or animating there. */
- private boolean mStackOnLeftOrWillBe = false;
+ private boolean mStackOnLeftOrWillBe = true;
/** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
private boolean mIsGestureInProgress = false;
@@ -936,9 +934,13 @@
mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
}
+ if (bubble.getIconView() == null) {
+ return;
+ }
+
// Set the dot position to the opposite of the side the stack is resting on, since the stack
// resting slightly off-screen would result in the dot also being off-screen.
- bubble.getIconView().setDotPosition(
+ bubble.getIconView().setDotPositionOnLeft(
!mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
mBubbleContainer.addView(bubble.getIconView(), 0,
@@ -1698,7 +1700,7 @@
|| mBubbleToExpandAfterFlyoutCollapse != null
|| bubbleView == null) {
if (bubbleView != null) {
- bubbleView.setDotState(DOT_STATE_DEFAULT);
+ bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
}
// 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, or if bubble view is null.
@@ -1717,12 +1719,16 @@
mBubbleData.setExpanded(true);
mBubbleToExpandAfterFlyoutCollapse = null;
}
- bubbleView.setDotState(DOT_STATE_DEFAULT);
+
+ // Stop suppressing the dot now that the flyout has morphed into the dot.
+ bubbleView.removeDotSuppressionFlag(
+ BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
};
mFlyout.setVisibility(INVISIBLE);
- // Don't show the dot when we're animating the flyout
- bubbleView.setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
+ // Suppress the dot when we are animating the flyout.
+ bubbleView.addDotSuppressionFlag(
+ BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
// Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
post(() -> {
@@ -1743,6 +1749,11 @@
};
mFlyout.postDelayed(mAnimateInFlyout, 200);
};
+
+ if (bubble.getIconView() == null) {
+ return;
+ }
+
mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
mStackAnimationController.getStackPosition(), getWidth(),
mStackAnimationController.isStackOnLeftSide(),
@@ -1877,9 +1888,19 @@
for (int i = 0; i < bubbleCount; i++) {
BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
bv.setZ((mMaxBubbles * mBubbleElevation) - i);
+
// If the dot is on the left, and so is the stack, we need to change the dot position.
if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
- bv.setDotPosition(!mStackOnLeftOrWillBe, animate);
+ bv.setDotPositionOnLeft(!mStackOnLeftOrWillBe, animate);
+ }
+
+ if (!mIsExpanded && i > 0) {
+ // If we're collapsed and this bubble is behind other bubbles, suppress its dot.
+ bv.addDotSuppressionFlag(
+ BadgedImageView.SuppressionFlag.BEHIND_STACK);
+ } else {
+ bv.removeDotSuppressionFlag(
+ BadgedImageView.SuppressionFlag.BEHIND_STACK);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index 7ee162e..00de8b4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -287,7 +287,7 @@
/** Whether the stack is on the left side of the screen. */
public boolean isStackOnLeftSide() {
if (mLayout == null || !isStackPositionSet()) {
- return false;
+ return true; // Default to left, which is where it starts by default.
}
float stackCenter = mStackPosition.x + mBubbleBitmapSize / 2;