From aa73f595cf4b60b5ffb98ef78186b6c8144207e4 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Mon, 6 May 2024 16:31:13 -0400 Subject: [PATCH] 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 --- .../taskbar/bubbles/BubbleBarController.java | 3 - .../bubbles/BubbleBarViewController.java | 15 ++- .../bubbles/BubbleStashController.java | 17 +-- .../animation/BubbleBarViewAnimator.kt | 58 ++++++++- .../animation/BubbleBarViewAnimatorTest.kt | 115 ++++++++++++++++++ 5 files changed, 189 insertions(+), 19 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index 66e530294c..8c83508ad9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -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(); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 0b74e15d27..ac02f1fe5e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -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!"); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java index 4b3416c0ce..d0462aa3da 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java @@ -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. * *

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. - * - *

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); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt index 66521c1117..be935d8b54 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -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 diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt index 70650750c2..2bcfa3f224 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt @@ -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