Animate the bubble bar for the first bubble

This change animates the bubble bar for the first bubble.
When we're in home -- the bubble bar bounces in and stays visible
When we're in app -- the bubble bar bounces in, then animates to
the stashed handle.
When the bubble bar auto expands, we currently animate the bubble
bar already expanded. In the future we'll time the expansion
until after the bubble bar settles in.

Demo: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/hIGwtb3YKyCT9Ke9adZNgY

Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT
Bug: 339066271
Test: atest BubbleBarViewAnimatorTest
Change-Id: I80ce55676b72a50bab23623e0b5c1e602690682f
This commit is contained in:
Liran Binyamin
2024-05-06 16:31:13 -04:00
parent b4b5e79ffe
commit aa73f595cf
5 changed files with 189 additions and 19 deletions
@@ -403,9 +403,6 @@ public class BubbleBarController extends IBubblesListener.Stub {
}
if (bubbleToSelect != null) {
setSelectedBubbleInternal(bubbleToSelect);
if (previouslySelectedBubble == null) {
mBubbleStashController.animateToInitialState(update.expanded);
}
}
if (update.shouldShowEducation) {
mBubbleBarViewController.prepareToShowEducation();
@@ -406,10 +406,21 @@ public class BubbleBarViewController {
return;
}
if (!(b instanceof BubbleBarBubble bubble)) {
return;
}
boolean isInApp = mTaskbarStashController.isInApp();
// if this is the first bubble, animate to the initial state. one bubble is the overflow
// so check for at most 2 children.
if (mBarView.getChildCount() <= 2) {
mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
return;
}
// only animate the new bubble if we're in an app and not auto expanding
if (b instanceof BubbleBarBubble && isInApp && !isExpanding && !isExpanded()) {
mBubbleBarViewAnimator.animateBubbleInForStashed((BubbleBarBubble) b);
if (isInApp && !isExpanding && !isExpanded()) {
mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
}
} else {
Log.w(TAG, "addBubble, bubble was null!");
@@ -123,21 +123,17 @@ public class BubbleStashController {
}
/**
* Animates the bubble bar and handle to their initial state, transitioning from the state where
* both views are invisible. Called when the first bubble is added or when the device is
* Animates the handle (or bubble bar depending on state) to be visible after the device is
* unlocked.
*
* <p>Normally either the bubble bar or the handle is visible,
* and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
* between these two states. But the transition from the state where both the bar and handle
* are invisible is slightly different.
*
* <p>The initial state will depend on the current state of the device, i.e. overview, home etc
* and whether bubbles are requested to be expanded.
*/
public void animateToInitialState(boolean expanding) {
private void animateAfterUnlock() {
AnimatorSet animatorSet = new AnimatorSet();
if (expanding || mBubblesShowingOnHome || mBubblesShowingOnOverview) {
if (mBubblesShowingOnHome || mBubblesShowingOnOverview) {
mIsStashed = false;
animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
@@ -217,7 +213,7 @@ public class BubbleStashController {
if (isSysuiLocked != mIsSysuiLocked) {
mIsSysuiLocked = isSysuiLocked;
if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
animateToInitialState(false /* expanding */);
animateAfterUnlock();
}
}
}
@@ -453,4 +449,9 @@ public class BubbleStashController {
mIsStashed = isStashed;
onIsStashedChanged();
}
/** Set the translation Y for the stashed handle. */
public void setHandleTranslationY(float ty) {
mHandleViewController.setTranslationYForSwipe(ty);
}
}
@@ -93,15 +93,15 @@ constructor(
if (animator.isRunning()) animator.cancel()
// the animation of a new bubble is divided into 2 parts. The first part shows the bubble
// and the second part hides it after a delay.
val showAnimation = buildShowAnimation()
val hideAnimation = buildHideAnimation()
val showAnimation = buildHandleToBubbleBarAnimation()
val hideAnimation = buildBubbleBarToHandleAnimation()
animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
/**
* Returns a [Runnable] that starts the animation that shows the new or updated bubble.
* Returns a [Runnable] that starts the animation that morphs the handle to the bubble bar.
*
* Visually, the animation is divided into 2 parts. The stash handle starts animating up and
* fading out and then the bubble bar starts animating up and fading in.
@@ -114,7 +114,7 @@ constructor(
* 3. The third part is the overshoot of the spring animation, where we make the bubble fully
* visible which helps avoiding further updates when we re-enter the second part.
*/
private fun buildShowAnimation() = Runnable {
private fun buildHandleToBubbleBarAnimation() = Runnable {
// prepare the bubble bar for the animation
bubbleBarView.onAnimatingBubbleStarted()
bubbleBarView.visibility = VISIBLE
@@ -197,7 +197,8 @@ constructor(
}
/**
* Returns a [Runnable] that starts the animation that hides the bubble bar.
* Returns a [Runnable] that starts the animation that hides the bubble bar and morphs it into
* the stashed handle.
*
* Similarly to the show animation, this is visually divided into 2 parts. We first animate the
* bubble bar out, and then animate the stash handle in. At the end of the animation we reset
@@ -209,13 +210,14 @@ constructor(
* 2. In the second part the bubble bar is fully hidden and the handle animates in.
* 3. The third part is the overshoot. The handle is made fully visible.
*/
private fun buildHideAnimation() = Runnable {
private fun buildBubbleBarToHandleAnimation() = Runnable {
if (animatingBubble == null) return@Runnable
val offset = bubbleStashController.diffBetweenHandleAndBarCenters
val stashedHandleTranslationY =
bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
// this is the total distance that both the stashed handle and the bar will be traveling
val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
bubbleStashController.setHandleTranslationY(totalTranslationY)
val animator = bubbleStashController.stashedHandlePhysicsAnimator
animator.setDefaultSpringConfig(springConfig)
animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
@@ -259,6 +261,50 @@ constructor(
animator.start()
}
/** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
// the animation of a new bubble is divided into 2 parts. The first part shows the bubble
// and the second part hides it after a delay if we are in an app.
val showAnimation = buildBubbleBarBounceAnimation()
val hideAnimation =
if (isInApp && !isExpanding) {
buildBubbleBarToHandleAnimation()
} else {
// in this case the bubble bar remains visible so not much to do. once we implement
// the flyout we'll update this runnable to hide it.
Runnable {
animatingBubble = null
bubbleStashController.showBubbleBarImmediate()
bubbleBarView.onAnimatingBubbleCompleted()
}
}
animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
private fun buildBubbleBarBounceAnimation() = Runnable {
// prepare the bubble bar for the animation
bubbleBarView.onAnimatingBubbleStarted()
bubbleBarView.translationY = bubbleBarView.height.toFloat()
bubbleBarView.visibility = VISIBLE
bubbleBarView.alpha = 1f
bubbleBarView.scaleX = 1f
bubbleBarView.scaleY = 1f
val animator = PhysicsAnimator.getInstance(bubbleBarView)
animator.setDefaultSpringConfig(springConfig)
animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
animator.addEndListener { _, _, _, _, _, _, _ ->
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
bubbleStashController.updateTaskbarTouchRegion()
}
animator.start()
}
/** Handles clicking on the animating bubble while the animation is still playing. */
fun onBubbleClickedWhileAnimating() {
val hideAnimation = animatingBubble?.hideAnimation ?: return
@@ -264,6 +264,120 @@ class BubbleBarViewAnimatorTest {
assertThat(animatorScheduler.delayedBlock).isNull()
}
@Test
fun animateToInitialState_inApp() {
setUpBubbleBar()
setUpBubbleStashController()
whenever(bubbleStashController.bubbleBarTranslationY)
.thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateToInitialState(bubble, isInApp = true, isExpanding = false)
}
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(barAnimator.isRunning()).isFalse()
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(handle.translationY).isEqualTo(0)
assertThat(handle.alpha).isEqualTo(1)
verify(bubbleStashController).stashBubbleBarImmediate()
}
@Test
fun animateToInitialState_inApp_autoExpanding() {
setUpBubbleBar()
setUpBubbleStashController()
whenever(bubbleStashController.bubbleBarTranslationY)
.thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateToInitialState(bubble, isInApp = true, isExpanding = true)
}
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(barAnimator.isRunning()).isFalse()
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
verify(bubbleStashController).showBubbleBarImmediate()
}
@Test
fun animateToInitialState_inHome() {
setUpBubbleBar()
setUpBubbleStashController()
whenever(bubbleStashController.bubbleBarTranslationY)
.thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
}
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(barAnimator.isRunning()).isFalse()
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
verify(bubbleStashController).showBubbleBarImmediate()
}
private fun setUpBubbleBar() {
bubbleBarView = BubbleBarView(context)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -322,3 +436,4 @@ class BubbleBarViewAnimatorTest {
private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f
private const val HANDLE_TRANSLATION = -30f
private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f
private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f