Merge "Add a new bubble at limit while expanded" into main

This commit is contained in:
Liran Binyamin
2024-06-27 17:26:34 +00:00
committed by Android (Google) Code Review
5 changed files with 241 additions and 37 deletions
@@ -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--) {
@@ -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 {
@@ -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());
@@ -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. */
@@ -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) {}