Add a new bubble at limit while expanded
Handles adding a new bubble and removing an old bubble when the bar
is expanded and at the limit.
Demo when bar is on right: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/dxhFKrctdR5I2F6Pvho6u8
Demo when bar is on left: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/er4ZGQfg8OKHZ2aTi9OJ9N
Flag: com.android.wm.shell.enable_bubble_bar
Test: demos
Test: atest BubbleViewAnimatorTest
Fixes: 345795791
Change-Id: I4e75d61c8afdb81340823a1d77e55b15e3fd6bc0
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 15e4578..f6b1328 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -322,27 +322,45 @@
|| mImeVisibilityChecker.isImeVisible();
BubbleBarBubble bubbleToSelect = null;
- 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) {
- mBubbleBarViewController.removeBubble(bubble);
- } else {
- Log.w(TAG, "trying to remove bubble that doesn't exist: "
- + removedBubble.getKey());
+
+ 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());
+ mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+ if (bubbleToRemove != null) {
+ mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble,
+ bubbleToRemove, isExpanding, suppressAnimation);
+ } else {
+ mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
+ suppressAnimation);
+ Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey());
+ }
+ } else {
+ 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) {
+ mBubbleBarViewController.removeBubble(bubble);
+ } else {
+ Log.w(TAG, "trying to remove bubble that doesn't exist: "
+ + removedBubble.getKey());
+ }
}
}
- }
- if (update.addedBubble != null) {
- mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
- mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, suppressAnimation);
- if (isCollapsed) {
- // If we're collapsed, the most recently added bubble will be selected.
- bubbleToSelect = update.addedBubble;
+ if (update.addedBubble != null) {
+ mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+ mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
+ suppressAnimation);
}
-
}
+
+ if (update.addedBubble != null && isCollapsed) {
+ // If we're collapsed, the most recently added bubble will be selected.
+ bubbleToSelect = update.addedBubble;
+ }
+
if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) {
// Iterate in reverse because new bubbles are added in front and the list is in order.
for (int i = update.currentBubbles.size() - 1; i >= 0; i--) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 0ea5031..07481a2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -626,6 +626,7 @@
/**
* Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
* respectively. If the value is not in range of 0 to 1 it will be normalized.
+ *
* @param x relative X pivot value in range 0..1
* @param y relative Y pivot value in range 0..1
*/
@@ -665,7 +666,9 @@
}
/** Add a new bubble to the bubble bar. */
- public void addBubble(View bubble, FrameLayout.LayoutParams lp) {
+ public void addBubble(View bubble) {
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
+ Gravity.LEFT);
if (isExpanded()) {
// if we're expanded scale the new bubble in
bubble.setScaleX(0f);
@@ -702,14 +705,58 @@
}
}
+ /** Add a new bubble and remove an old bubble from the bubble bar. */
+ public void addBubbleAndRemoveBubble(View addedBubble, View removedBubble) {
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
+ Gravity.LEFT);
+ if (!isExpanded()) {
+ removeView(removedBubble);
+ addView(addedBubble, 0, lp);
+ return;
+ }
+ addedBubble.setScaleX(0f);
+ addedBubble.setScaleY(0f);
+ addView(addedBubble, 0, lp);
+
+ int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
+ int indexOfBubbleToRemove = indexOfChild(removedBubble);
+
+ mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
+ getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
+ BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
+
+ @Override
+ public void onAnimationEnd() {
+ removeView(removedBubble);
+ updateWidth();
+ mBubbleAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ addedBubble.setScaleX(1);
+ addedBubble.setScaleY(1);
+ removedBubble.setScaleX(0);
+ removedBubble.setScaleY(0);
+ }
+
+ @Override
+ public void onAnimationUpdate(float animatedFraction) {
+ addedBubble.setScaleX(animatedFraction);
+ addedBubble.setScaleY(animatedFraction);
+ removedBubble.setScaleX(1 - animatedFraction);
+ removedBubble.setScaleY(1 - animatedFraction);
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+ };
+ mBubbleAnimator.animateNewAndRemoveOld(indexOfSelectedBubble, indexOfBubbleToRemove,
+ listener);
+ }
+
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
- if (getChildCount() + 1 > MAX_BUBBLES) {
- // the last child view is the overflow bubble and we shouldn't remove that. remove the
- // second to last child view.
- removeViewInLayout(getChildAt(getChildCount() - 2));
- }
super.addView(child, index, params);
updateWidth();
updateBubbleAccessibilityStates();
@@ -911,7 +958,7 @@
final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing;
float translationX;
if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
- return mBubbleAnimator.getExpandedBubbleTranslationX(bubbleIndex) + mBubbleBarPadding;
+ return mBubbleAnimator.getBubbleTranslationX(bubbleIndex) + mBubbleBarPadding;
} else if (onLeft) {
translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing;
} else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index da0826b..dbc78db 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -25,10 +25,8 @@
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
-import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
-import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -386,7 +384,7 @@
/**
* Removes the provided bubble from the bubble bar.
*/
- public void removeBubble(BubbleBarItem b) {
+ public void removeBubble(BubbleBarBubble b) {
if (b != null) {
mBarView.removeBubble(b.getView());
} else {
@@ -394,13 +392,23 @@
}
}
+ /** Adds a new bubble and removes an old bubble at the same time. */
+ public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble,
+ BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation) {
+ mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView());
+ addedBubble.getView().setOnClickListener(mBubbleClickListener);
+ mBubbleDragController.setupBubbleView(addedBubble.getView());
+ if (!suppressAnimation) {
+ animateBubbleNotification(addedBubble, isExpanding);
+ }
+ }
+
/**
* Adds the provided bubble to the bubble bar.
*/
public void addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation) {
if (b != null) {
- mBarView.addBubble(
- b.getView(), new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT));
+ mBarView.addBubble(b.getView());
b.getView().setOnClickListener(mBubbleClickListener);
mBubbleDragController.setupBubbleView(b.getView());
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
index 7672743..8af8ffb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
@@ -56,6 +56,20 @@
animator.start()
}
+ fun animateNewAndRemoveOld(
+ selectedBubbleIndex: Int,
+ removedBubbleIndex: Int,
+ listener: Listener
+ ) {
+ animator = createAnimator(listener)
+ state =
+ State.AddingAndRemoving(
+ selectedBubbleIndex = selectedBubbleIndex,
+ removedBubbleIndex = removedBubbleIndex
+ )
+ animator.start()
+ }
+
private fun createAnimator(listener: Listener): ValueAnimator {
val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
animator.addUpdateListener { animation ->
@@ -83,28 +97,35 @@
}
/**
- * The translation X of the bubble at index [bubbleIndex] according to the progress of the
- * animation.
+ * The translation X of the bubble at index [bubbleIndex] when the bubble bar is expanded
+ * according to the progress of this animation.
*
* Callers should verify that the animation is running before calling this.
*
* @see isRunning
*/
- fun getExpandedBubbleTranslationX(bubbleIndex: Int): Float {
+ fun getBubbleTranslationX(bubbleIndex: Int): Float {
return when (val state = state) {
State.Idle -> 0f
is State.AddingBubble ->
- getExpandedBubbleTranslationXWhileScalingBubble(
+ getBubbleTranslationXWhileScalingBubble(
bubbleIndex = bubbleIndex,
scalingBubbleIndex = 0,
bubbleScale = animator.animatedFraction
)
is State.RemovingBubble ->
- getExpandedBubbleTranslationXWhileScalingBubble(
+ getBubbleTranslationXWhileScalingBubble(
bubbleIndex = bubbleIndex,
scalingBubbleIndex = state.bubbleIndex,
bubbleScale = 1 - animator.animatedFraction
)
+ is State.AddingAndRemoving ->
+ getBubbleTranslationXWhileAddingBubbleAtLimit(
+ bubbleIndex = bubbleIndex,
+ removedBubbleIndex = state.removedBubbleIndex,
+ addedBubbleScale = animator.animatedFraction,
+ removedBubbleScale = 1 - animator.animatedFraction
+ )
}
}
@@ -121,6 +142,14 @@
State.Idle -> 0f
is State.AddingBubble -> animator.animatedFraction
is State.RemovingBubble -> 1 - animator.animatedFraction
+ is State.AddingAndRemoving -> {
+ // since we're adding a bubble and removing another bubble, their sizes together
+ // equal to a single bubble. the width is the same as having bubbleCount - 1
+ // bubbles at full scale.
+ val totalSpace = (bubbleCount - 2) * expandedBarIconSpacing
+ val totalIconSize = (bubbleCount - 1) * iconSize
+ return totalIconSize + totalSpace
+ }
}
// When this animator is running the bubble bar is expanded so it's safe to assume that we
// have at least 2 bubbles, but should update the logic to support optional overflow.
@@ -144,7 +173,7 @@
State.Idle -> 0f
is State.AddingBubble -> {
val tx =
- getExpandedBubbleTranslationXWhileScalingBubble(
+ getBubbleTranslationXWhileScalingBubble(
bubbleIndex = state.selectedBubbleIndex,
scalingBubbleIndex = 0,
bubbleScale = animator.animatedFraction
@@ -152,6 +181,17 @@
tx + iconSize / 2f
}
is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state)
+ is State.AddingAndRemoving -> {
+ // we never remove the selected bubble, so the arrow stays pointing to its center
+ val tx =
+ getBubbleTranslationXWhileAddingBubbleAtLimit(
+ bubbleIndex = state.selectedBubbleIndex,
+ removedBubbleIndex = state.removedBubbleIndex,
+ addedBubbleScale = animator.animatedFraction,
+ removedBubbleScale = 1 - animator.animatedFraction
+ )
+ tx + iconSize / 2f
+ }
}
}
@@ -160,7 +200,7 @@
// if we're not removing the selected bubble, the selected bubble doesn't change so just
// return the translation X of the selected bubble and add half icon
val tx =
- getExpandedBubbleTranslationXWhileScalingBubble(
+ getBubbleTranslationXWhileScalingBubble(
bubbleIndex = state.selectedBubbleIndex,
scalingBubbleIndex = state.bubbleIndex,
bubbleScale = 1 - animator.animatedFraction
@@ -208,7 +248,7 @@
* @param scalingBubbleIndex the index of the bubble that is animating
* @param bubbleScale the current scale of the animating bubble
*/
- private fun getExpandedBubbleTranslationXWhileScalingBubble(
+ private fun getBubbleTranslationXWhileScalingBubble(
bubbleIndex: Int,
scalingBubbleIndex: Int,
bubbleScale: Float
@@ -256,6 +296,68 @@
}
}
+ private fun getBubbleTranslationXWhileAddingBubbleAtLimit(
+ bubbleIndex: Int,
+ removedBubbleIndex: Int,
+ addedBubbleScale: Float,
+ removedBubbleScale: Float
+ ): Float {
+ val iconAndSpacing = iconSize + expandedBarIconSpacing
+ // the bubbles are scaling from the center, so we need to adjust their translation so
+ // that the distance to the adjacent bubble scales at the same rate.
+ val addedBubblePivotAdjustment = -(1 - addedBubbleScale) * iconSize / 2f
+ val removedBubblePivotAdjustment = -(1 - removedBubbleScale) * iconSize / 2f
+
+ return if (onLeft) {
+ // this is how many bubbles there are to the left of the current bubble.
+ // when the bubble bar is on the right the added bubble is the right-most bubble so it
+ // doesn't affect the translation of any other bubble.
+ // when the removed bubble is to the left of the current bubble, we need to subtract it
+ // from bubblesToLeft and use removedBubbleScale instead when calculating the
+ // translation.
+ val bubblesToLeft = bubbleCount - bubbleIndex - 1
+ when {
+ bubbleIndex == 0 ->
+ // this is the added bubble and it's the right-most bubble. account for all the
+ // other bubbles -- including the removed bubble -- and adjust for the added
+ // bubble pivot.
+ (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing +
+ addedBubblePivotAdjustment
+ bubbleIndex < removedBubbleIndex ->
+ // the removed bubble is to the left so account for it
+ (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing
+ bubbleIndex == removedBubbleIndex -> {
+ // this is the removed bubble. all the bubbles to the left are at full scale
+ // but we need to scale the spacing between the removed bubble and the bubble to
+ // its left because the removed bubble disappears towards the left side
+ val totalIconSize = bubblesToLeft * iconSize
+ val totalSpacing =
+ (bubblesToLeft - 1 + removedBubbleScale) * expandedBarIconSpacing
+ totalIconSize + totalSpacing + removedBubblePivotAdjustment
+ }
+ else ->
+ // both added and removed bubbles are to the right so they don't affect the tx
+ bubblesToLeft * iconAndSpacing
+ }
+ } else {
+ when {
+ bubbleIndex == 0 -> addedBubblePivotAdjustment // we always add bubbles at index 0
+ bubbleIndex < removedBubbleIndex ->
+ // the bar is on the right and the removed bubble is on the right. the current
+ // bubble is unaffected by the removed bubble. only need to factor in the added
+ // bubble's scale.
+ iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale)
+ bubbleIndex == removedBubbleIndex ->
+ // the bar is on the right, and this is the animating bubble.
+ iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale) +
+ removedBubblePivotAdjustment
+ else ->
+ // both the added and the removed bubbles are to the left of the current bubble
+ iconAndSpacing * (bubbleIndex - 2 + addedBubbleScale + removedBubbleScale)
+ }
+ }
+ }
+
val isRunning: Boolean
get() = state != State.Idle
@@ -277,6 +379,10 @@
/** Whether the bubble being removed is also the last bubble. */
val removingLastBubble: Boolean
) : State
+
+ /** A new bubble is being added and an old bubble is being removed from the bubble bar. */
+ data class AddingAndRemoving(val selectedBubbleIndex: Int, val removedBubbleIndex: Int) :
+ State
}
/** Callbacks for the animation. */
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
index 20bd617..d5a76a2 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
@@ -80,6 +80,31 @@
assertThat(bubbleAnimator.isRunning).isFalse()
}
+ @Test
+ fun animateNewAndRemoveOld_isRunning() {
+ bubbleAnimator =
+ BubbleAnimator(
+ iconSize = 40f,
+ expandedBarIconSpacing = 10f,
+ bubbleCount = 5,
+ onLeft = false
+ )
+ val listener = TestBubbleAnimatorListener()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleAnimator.animateNewAndRemoveOld(
+ selectedBubbleIndex = 3,
+ removedBubbleIndex = 2,
+ listener
+ )
+ }
+
+ assertThat(bubbleAnimator.isRunning).isTrue()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+ assertThat(bubbleAnimator.isRunning).isFalse()
+ }
+
private class TestBubbleAnimatorListener : BubbleAnimator.Listener {
override fun onAnimationUpdate(animatedFraction: Float) {}