Implement new bubble animation when collapsed

Bounce the bubble bar when a new bubble is received while the bubble
bar is collapsed.

Demo: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/fn7NWNY3htuR6K3wxhfcK2

Flag: com.android.wm.shell.enable_bubble_bar
Bug: 280605790
Test: atest BubbleBarViewAnimatorTest
Change-Id: I4c622454fd99f6bb5a332b3fe4aa2764c8af93af
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 5c82c99..d4f66e2 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -445,6 +445,7 @@
     <dimen name="bubblebar_elevation">1dp</dimen>
     <dimen name="bubblebar_drag_elevation">2dp</dimen>
     <dimen name="bubblebar_hotseat_adjustment_threshold">90dp</dimen>
+    <dimen name="bubblebar_bounce_distance">20dp</dimen>
 
     <dimen name="bubblebar_icon_size_small">32dp</dimen>
     <dimen name="bubblebar_icon_size">36dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index eec095d..951b99d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -435,6 +435,11 @@
             return;
         }
 
+        if (mBubbleStashController.isBubblesShowingOnHome() && !isExpanding && !isExpanded()) {
+            mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble);
+            return;
+        }
+
         // only animate the new bubble if we're in an app and not auto expanding
         if (isInApp && !isExpanding && !isExpanded()) {
             mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
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 d88e272..2dcd932 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -18,8 +18,12 @@
 
 import android.view.View
 import android.view.View.VISIBLE
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.ObjectAnimator
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.SpringForce
+import com.android.launcher3.R
 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleStashController
@@ -36,6 +40,8 @@
 ) {
 
     private var animatingBubble: AnimatingBubble? = null
+    private val bubbleBarBounceDistanceInPx =
+            bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
 
     private companion object {
         /** The time to show the flyout. */
@@ -44,6 +50,8 @@
         const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f
         /** The minimum alpha value to make the bubble bar touchable. */
         const val MIN_ALPHA_FOR_TOUCHABLE = 0.5f
+        /** The duration of the bounce animation. */
+        const val BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS = 250L
     }
 
     /** Wrapper around the animating bubble with its show and hide animations. */
@@ -277,7 +285,7 @@
         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 showAnimation = buildBubbleBarSpringInAnimation()
         val hideAnimation =
             if (isInApp && !isExpanding) {
                 buildBubbleBarToHandleAnimation()
@@ -296,7 +304,7 @@
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
 
-    private fun buildBubbleBarBounceAnimation() = Runnable {
+    private fun buildBubbleBarSpringInAnimation() = Runnable {
         // prepare the bubble bar for the animation
         bubbleBarView.onAnimatingBubbleStarted()
         bubbleBarView.translationY = bubbleBarView.height.toFloat()
@@ -316,6 +324,42 @@
         animator.start()
     }
 
+    fun animateBubbleBarForCollapsed(b: BubbleBarBubble) {
+        val bubbleView = b.view
+        val animator = PhysicsAnimator.getInstance(bubbleView)
+        if (animator.isRunning()) animator.cancel()
+        val showAnimation = buildBubbleBarBounceAnimation()
+        val hideAnimation = Runnable {
+            animatingBubble = null
+            bubbleStashController.showBubbleBarImmediate()
+            bubbleBarView.onAnimatingBubbleCompleted()
+            bubbleStashController.updateTaskbarTouchRegion()
+        }
+        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        scheduler.post(showAnimation)
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+    }
+
+    /**
+     * The bubble bar animation when it is collapsed is divided into 2 chained animations. The first
+     * animation is a regular accelerate animation that moves the bubble bar upwards. When it ends
+     * the bubble bar moves back to its initial position with a spring animation.
+     */
+    private fun buildBubbleBarBounceAnimation() = Runnable {
+        bubbleBarView.onAnimatingBubbleStarted()
+        val ty = bubbleBarView.translationY
+
+        val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
+        springBackAnimation.setDefaultSpringConfig(springConfig)
+        springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
+
+        // animate the bubble bar up and start the spring back down animation when it ends.
+        ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
+            .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
+            .withEndAction { springBackAnimation.start() }
+            .start()
+    }
+
     /** Handles touching the animating bubble bar. */
     fun onBubbleBarTouchedWhileAnimating() {
         PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
@@ -344,4 +388,20 @@
     private fun <T> PhysicsAnimator<T>.cancelIfRunning() {
         if (isRunning()) cancel()
     }
+
+    private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator {
+        setDuration(duration)
+        return this
+    }
+
+    private fun ObjectAnimator.withEndAction(endAction: () -> Unit): ObjectAnimator {
+        addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    endAction()
+                }
+            }
+        )
+        return this
+    }
 }
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 cc579ab..2ae4e6b 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
@@ -24,6 +24,7 @@
 import android.view.View
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
+import androidx.core.animation.AnimatorTestRule
 import androidx.core.graphics.drawable.toBitmap
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.test.core.app.ApplicationProvider
@@ -41,6 +42,7 @@
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
@@ -53,6 +55,8 @@
 @RunWith(AndroidJUnit4::class)
 class BubbleBarViewAnimatorTest {
 
+    @get:Rule val animatorTestRule = AnimatorTestRule()
+
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler
     private lateinit var overflowView: BubbleView
@@ -380,6 +384,45 @@
         verify(bubbleStashController).showBubbleBarImmediate()
     }
 
+    @Test
+    fun animateBubbleBarForCollapsed() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        bubbleBarView.translationY = BAR_TRANSLATION_Y_FOR_HOTSEAT
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleBarForCollapsed(bubble)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // verify we started animating
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        // the bubble bar translation y should be back to its initial value
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {