Support optional bubble overflow in bubble bar
This is similar to the animations that add / remove a bubble at the
same time -- the overflow is generally added when a bubble is removed.
The overflow is generally removed when a bubble is added (i.e. user
promotes a bubble out of the overflow).
There are a couple of additional cases:
- when bubbles are first added to the bar -- if there were saved
bubbles in the overflow, the view should be added
- an app could cancel its bubbles / remove its shortcuts and not have
any in the stack but could have some in the overflow & it could
become empty without an addition.
Flag: com.android.wm.shell.enable_optional_bubble_overflow
Flag: com.android.wm.shell.enable_bubble_bar
Test: manual - add bubbles to the bubble bar for first time
=> observe there is no overflow
- dismiss a bubble
=> observe the overflow is added, tap on it, tap on the
bubble in it
=> observe that bubble is added & the overflow disappears
- dismiss all the bubbles
- add a bubble
=> observe the overflow is there & has the previously
dismissed bubbles
- cancel all the bubbles that are in the overflow via
adb
=> observe the overflow is remvoed
Bug: 334175587
Change-Id: I2b6e855e65520b4b2b1fde7757d46f00a468b4a6
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index fbee080..33d8a84 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -138,6 +138,8 @@
List<RemovedBubble> removedBubbles;
List<String> bubbleKeysInOrder;
Point expandedViewDropTargetSize;
+ boolean showOverflow;
+ boolean showOverflowChanged;
// These need to be loaded in the background
BubbleBarBubble addedBubble;
@@ -156,6 +158,8 @@
removedBubbles = update.removedBubbles;
bubbleKeysInOrder = update.bubbleKeysInOrder;
expandedViewDropTargetSize = update.expandedViewDropTargetSize;
+ showOverflow = update.showOverflow;
+ showOverflowChanged = update.showOverflowChanged;
}
}
@@ -271,7 +275,13 @@
BubbleBarBubble bubbleToSelect = null;
- if (update.addedBubble != null && update.removedBubbles.size() == 1) {
+ if (Flags.enableOptionalBubbleOverflow()
+ && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null
+ && update.removedBubbles.isEmpty()) {
+ // A bubble was added from the overflow (& now it's empty / not showing)
+ mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+ mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble);
+ } else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
// we're adding and removing a bubble at the same time. handle this as a single update.
RemovedBubble removedBubble = update.removedBubbles.get(0);
BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey());
@@ -285,11 +295,17 @@
Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey());
}
} else {
+ boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow()
+ && update.showOverflowChanged && update.showOverflow;
if (!update.removedBubbles.isEmpty()) {
for (int i = 0; i < update.removedBubbles.size(); i++) {
RemovedBubble removedBubble = update.removedBubbles.get(i);
BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
- if (bubble != null) {
+ if (bubble != null && overflowNeedsToBeAdded) {
+ // First removal, show the overflow
+ overflowNeedsToBeAdded = false;
+ mBubbleBarViewController.addOverflowAndRemoveBubble(bubble);
+ } else if (bubble != null) {
mBubbleBarViewController.removeBubble(bubble);
} else {
Log.w(TAG, "trying to remove bubble that doesn't exist: "
@@ -302,6 +318,11 @@
mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
suppressAnimation);
}
+ if (Flags.enableOptionalBubbleOverflow()
+ && update.showOverflowChanged
+ && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) {
+ mBubbleBarViewController.showOverflow(update.showOverflow);
+ }
}
// if a bubble was updated upstream, but removed before the update was received, add it back
@@ -333,6 +354,9 @@
}
}
}
+ if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) {
+ mBubbleBarViewController.showOverflow(true);
+ }
// Adds and removals have happened, update visibility before any other visual changes.
mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 7d27a90..32ca9f2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -715,11 +715,13 @@
public void addBubble(BubbleView bubble) {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
Gravity.LEFT);
+ final int index = bubble.isOverflow() ? getChildCount() : 0;
+
if (isExpanded()) {
// if we're expanded scale the new bubble in
bubble.setScaleX(0f);
bubble.setScaleY(0f);
- addView(bubble, 0, lp);
+ addView(bubble, index, lp);
bubble.showDotIfNeeded(/* animate= */ false);
mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
@@ -748,23 +750,33 @@
};
mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener);
} else {
- addView(bubble, 0, lp);
+ addView(bubble, index, lp);
}
}
/** Add a new bubble and remove an old bubble from the bubble bar. */
- public void addBubbleAndRemoveBubble(View addedBubble, View removedBubble) {
+ public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble) {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
Gravity.LEFT);
+ boolean isOverflowSelected = mSelectedBubbleView.isOverflow();
+ boolean removingOverflow = removedBubble.isOverflow();
+ boolean addingOverflow = addedBubble.isOverflow();
+
if (!isExpanded()) {
removeView(removedBubble);
- addView(addedBubble, 0, lp);
+ int index = addingOverflow ? getChildCount() : 0;
+ addView(addedBubble, index, lp);
return;
}
+ int index = addingOverflow ? getChildCount() : 0;
addedBubble.setScaleX(0f);
addedBubble.setScaleY(0f);
- addView(addedBubble, 0, lp);
+ addView(addedBubble, index, lp);
+ if (isOverflowSelected && removingOverflow) {
+ // The added bubble will be selected
+ mSelectedBubbleView = addedBubble;
+ }
int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
int indexOfBubbleToRemove = indexOfChild(removedBubble);
@@ -924,7 +936,7 @@
final float currentWidth = getWidth();
final float expandedWidth = expandedWidth();
final float collapsedWidth = collapsedWidth();
- int bubbleCount = getChildCount();
+ int childCount = getChildCount();
float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0);
float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight();
// When translating X & Y the scale is ignored, so need to deduct it from the translations
@@ -932,7 +944,7 @@
final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
// elevation state is opposite to widthState - when expanded all icons are flat
float elevationState = (1 - widthState);
- for (int i = 0; i < bubbleCount; i++) {
+ for (int i = 0; i < childCount; i++) {
BubbleView bv = (BubbleView) getChildAt(i);
if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) {
// Skip the dragged bubble. Its translation is managed by the drag controller.
@@ -951,9 +963,9 @@
bv.setTranslationY(ty);
// the position of the bubble when the bar is fully expanded
- final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
+ final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft);
// the position of the bubble when the bar is fully collapsed
- final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft);
+ final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft);
// slowly animate elevation while keeping correct Z ordering
float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
@@ -981,13 +993,10 @@
final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
final float targetX = collapsedX + collapsedBarShift;
bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
- // If we're fully collapsed, hide all bubbles except for the first 2. If there are
- // only 2 bubbles, hide the second bubble as well because it's the overflow.
+ // If we're fully collapsed, hide all bubbles except for the first 2, excluding
+ // the overflow.
if (widthState == 0) {
- if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
- bv.setAlpha(0);
- } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1
- && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) {
+ if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
bv.setAlpha(0);
} else {
bv.setAlpha(1);
@@ -1043,22 +1052,26 @@
return translationX - getScaleIconShift();
}
- private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) {
- if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
+ private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) {
+ if (bubbleIndex < 0 || bubbleIndex >= childCount) {
return 0;
}
float translationX;
if (onLeft) {
- // Shift the first bubble only if there are more bubbles in addition to overflow
- translationX = mBubbleBarPadding + (
- bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED
- ? mIconOverlapAmount : 0);
+ // Shift the first bubble only if there are more bubbles
+ if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
+ translationX = mIconOverlapAmount;
+ } else {
+ translationX = 0f;
+ }
} else {
- translationX = mBubbleBarPadding + (
- bubbleIndex == 0 || bubbleCount <= MAX_VISIBLE_BUBBLES_COLLAPSED
- ? 0 : mIconOverlapAmount);
+ if (bubbleIndex == 1 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
+ translationX = mIconOverlapAmount;
+ } else {
+ translationX = 0f;
+ }
}
- return translationX - getScaleIconShift();
+ return mBubbleBarPadding + translationX - getScaleIconShift();
}
/**
@@ -1256,15 +1269,20 @@
}
private float collapsedWidth() {
- final int childCount = getChildCount();
+ final int bubbleChildCount = getBubbleChildCount();
final float horizontalPadding = 2 * mBubbleBarPadding;
- // If there are more than 2 bubbles, the first 2 should be visible when collapsed.
- // Otherwise just the first bubble should be visible because we don't show the overflow.
- return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED
+ // If there are more than 2 bubbles, the first 2 should be visible when collapsed,
+ // excluding the overflow.
+ return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED
? getScaledIconSize() + mIconOverlapAmount + horizontalPadding
: getScaledIconSize() + horizontalPadding;
}
+ /** Returns the child count excluding the overflow if it's present. */
+ private int getBubbleChildCount() {
+ return hasOverflow() ? getChildCount() - 1 : getChildCount();
+ }
+
private float getBubbleBarExpandedHeight() {
return getBubbleBarCollapsedHeight() + mPointerSize;
}
@@ -1303,8 +1321,8 @@
return mIsAnimatingNewBubble;
}
- private boolean hasOverview() {
- // Overview is always the last bubble
+ private boolean hasOverflow() {
+ // Overflow is always the last bubble
View lastChild = getChildAt(getChildCount() - 1);
if (lastChild instanceof BubbleView bubbleView) {
return bubbleView.getBubble() instanceof BubbleBarOverflow;
@@ -1336,7 +1354,7 @@
CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : "";
// Don't count overflow if it exists
- int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0);
+ int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0);
if (bubbleCount > 1) {
contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles,
contentDesc, bubbleCount - 1);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 590916e..2cdc0ce 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -92,6 +92,8 @@
private boolean mHiddenForNoBubbles = true;
private boolean mShouldShowEducation;
+ public boolean mOverflowAdded;
+
private BubbleBarViewAnimator mBubbleBarViewAnimator;
private final TimeSource mTimeSource = System::currentTimeMillis;
@@ -123,7 +125,6 @@
mBubbleBarClickListener = v -> expandBubbleBar();
mBubbleDragController.setupBubbleBarView(mBarView);
mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
- addOverflow();
mBarView.setOnClickListener(mBubbleBarClickListener);
mBarView.addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -494,13 +495,44 @@
}
}
- /**
- * Adds the overflow view to the bubble bar.
- */
- public void addOverflow() {
- mBarView.addBubble(mOverflowBubble.getView());
+ /** Whether the overflow view is added to the bubble bar. */
+ public boolean isOverflowAdded() {
+ return mOverflowAdded;
+ }
+
+ /** Shows or hides the overflow view. */
+ public void showOverflow(boolean showOverflow) {
+ if (mOverflowAdded == showOverflow) return;
+ mOverflowAdded = showOverflow;
+ if (mOverflowAdded) {
+ mBarView.addBubble(mOverflowBubble.getView());
+ mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
+ mOverflowBubble.getView().setController(mBubbleViewController);
+ } else {
+ mBarView.removeBubble(mOverflowBubble.getView());
+ mOverflowBubble.getView().setOnClickListener(null);
+ mOverflowBubble.getView().setController(null);
+ }
+ }
+
+ /** Adds the overflow view to the bubble bar while animating a view away. */
+ public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) {
+ if (mOverflowAdded) return;
+ mOverflowAdded = true;
+ mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView());
mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
mOverflowBubble.getView().setController(mBubbleViewController);
+ removedBubble.getView().setController(null);
+ }
+
+ /** Removes the overflow view to the bubble bar while animating a view in. */
+ public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) {
+ if (!mOverflowAdded) return;
+ mOverflowAdded = false;
+ mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView());
+ addedBubble.getView().setOnClickListener(mBubbleClickListener);
+ addedBubble.getView().setController(mBubbleViewController);
+ mOverflowBubble.getView().setController(null);
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 09da3e0..f0f2872 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -74,6 +74,7 @@
private boolean mOnLeft = false;
private BubbleBarItem mBubble;
+ private boolean mIsOverflow;
private Bitmap mIcon;
@@ -271,12 +272,18 @@
*/
public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
mBubble = overflow;
+ mIsOverflow = true;
mIcon = bitmap;
updateBubbleIcon();
mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description));
}
+ /** Whether this view represents the overflow button. */
+ public boolean isOverflow() {
+ return mIsOverflow;
+ }
+
/** Returns the bubble being rendered in this view. */
@Nullable
public BubbleBarItem getBubble() {