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 {