Merge "Add a new bubble at limit while expanded" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
fde4fdc472
@@ -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. */
|
||||
|
||||
+25
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user