From 2e1dd4644b8dbc4d7411c11a71dc37f66eee3a90 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Wed, 26 Jun 2024 12:35:18 -0400 Subject: [PATCH] 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 --- .../taskbar/bubbles/BubbleBarController.java | 52 +++++--- .../taskbar/bubbles/BubbleBarView.java | 61 ++++++++- .../bubbles/BubbleBarViewController.java | 18 ++- .../bubbles/animation/BubbleAnimator.kt | 122 ++++++++++++++++-- .../bubbles/animation/BubbleAnimatorTest.kt | 25 ++++ 5 files changed, 241 insertions(+), 37 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index 15e4578d7e..f6b1328a10 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 @@ public class BubbleBarController extends IBubblesListener.Stub { || 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 0ea5031147..07481a2797 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 @@ public class BubbleBarView extends FrameLayout { /** * 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 @@ public class BubbleBarView extends FrameLayout { } /** 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 @@ public class BubbleBarView extends FrameLayout { } } + /** 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 @@ public class BubbleBarView extends FrameLayout { 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 da0826be0b..dbc78db8e1 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.graphics.Rect; 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 @@ public class BubbleBarViewController { /** * 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 @@ public class BubbleBarViewController { } } + /** 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 76727438ec..8af8ffb22a 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 @@ class BubbleAnimator( 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 @@ class BubbleAnimator( } /** - * 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 @@ class BubbleAnimator( 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 @@ class BubbleAnimator( State.Idle -> 0f is State.AddingBubble -> { val tx = - getExpandedBubbleTranslationXWhileScalingBubble( + getBubbleTranslationXWhileScalingBubble( bubbleIndex = state.selectedBubbleIndex, scalingBubbleIndex = 0, bubbleScale = animator.animatedFraction @@ -152,6 +181,17 @@ class BubbleAnimator( 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 @@ class BubbleAnimator( // 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 @@ class BubbleAnimator( * @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 @@ class BubbleAnimator( } } + 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 @@ class BubbleAnimator( /** 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 20bd617173..d5a76a2f4c 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 @@ class BubbleAnimatorTest { 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) {}