Interrupt bubble animation on stash change
This change handles cancelling the currently running bubble animation when the stash state is changing.
Demo - http://recall/-/bJtug1HhvXkkeA4MQvIaiP/4jnBgnFaIPez6m7fVLSlf
Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT
Bug: 280605846
Test: atest BubbleBarViewAnimatorTest
Change-Id: I34628f8ad741228dd21285ad66e45ef2909fbdab
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0b92748..715a4d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -172,6 +172,13 @@
}
}
+ /** Notifies that the stash state is changing. */
+ public void onStashStateChanging() {
+ if (isAnimatingNewBubble()) {
+ mBubbleBarViewAnimator.onStashStateChangingWhileAnimating();
+ }
+ }
+
//
// The below animators are exposed to BubbleStashController so it can manage the stashing
// animation.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index f689a05..4b3416c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -250,6 +250,11 @@
&& !mBubblesShowingOnHome
&& !mBubblesShowingOnOverview;
if (mIsStashed != isStashed) {
+ // notify the view controller that the stash state is about to change so that it can
+ // cancel an ongoing animation if there is one.
+ // note that this has to be called before updating mIsStashed with the new value,
+ // otherwise interrupting an ongoing animation may update it again with the wrong state
+ mBarViewController.onStashStateChanging();
mIsStashed = isStashed;
if (mAnimator != null) {
mAnimator.cancel();
@@ -423,4 +428,29 @@
mIsStashed = true;
onIsStashedChanged();
}
+
+ /**
+ * Updates the values of the internal animators after the new bubble animation was interrupted
+ *
+ * @param isStashed whether the current state should be stashed
+ * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
+ * bubble bar is showing to ensure that the stash animator runs
+ * smoothly.
+ */
+ public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) {
+ if (isStashed) {
+ mBubbleStashedHandleAlpha.setValue(1);
+ mIconAlphaForStash.setValue(0);
+ mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
+ mIconTranslationYForStash.updateValue(getStashTranslation());
+ } else {
+ mBubbleStashedHandleAlpha.setValue(0);
+ mHandleViewController.setTranslationYForSwipe(0);
+ mIconAlphaForStash.setValue(1);
+ mIconScaleForStash.updateValue(1);
+ mIconTranslationYForStash.updateValue(bubbleBarTranslationY);
+ }
+ mIsStashed = isStashed;
+ onIsStashedChanged();
+ }
}
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 a6d0ff8..66521c1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -179,7 +179,17 @@
}
}
}
- animator.addEndListener { _, _, _, _, _, _, _ ->
+ animator.addEndListener { _, _, _, canceled, _, _, _ ->
+ // if the show animation was canceled, also cancel the hide animation. this is typically
+ // canceled in this class, but could potentially be canceled elsewhere.
+ if (canceled) {
+ val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
+ scheduler.cancel(hideAnimation)
+ animatingBubble = null
+ bubbleBarView.onAnimatingBubbleCompleted()
+ bubbleBarView.relativePivotY = 1f
+ return@addEndListener
+ }
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -200,6 +210,7 @@
* 3. The third part is the overshoot. The handle is made fully visible.
*/
private fun buildHideAnimation() = Runnable {
+ if (animatingBubble == null) return@Runnable
val offset = bubbleStashController.diffBetweenHandleAndBarCenters
val stashedHandleTranslationY =
bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
@@ -238,9 +249,9 @@
}
}
}
- animator.addEndListener { _, _, _, _, _, _, _ ->
+ animator.addEndListener { _, _, _, canceled, _, _, _ ->
animatingBubble = null
- bubbleStashController.stashBubbleBarImmediate()
+ if (!canceled) bubbleStashController.stashBubbleBarImmediate()
bubbleBarView.onAnimatingBubbleCompleted()
bubbleBarView.relativePivotY = 1f
bubbleStashController.updateTaskbarTouchRegion()
@@ -256,4 +267,18 @@
bubbleBarView.relativePivotY = 1f
animatingBubble = null
}
+
+ /** Notifies the animator that the taskbar area was touched during an animation. */
+ fun onStashStateChangingWhileAnimating() {
+ val hideAnimation = animatingBubble?.hideAnimation ?: return
+ scheduler.cancel(hideAnimation)
+ animatingBubble = null
+ bubbleStashController.stashedHandlePhysicsAnimator.cancel()
+ bubbleBarView.onAnimatingBubbleCompleted()
+ bubbleBarView.relativePivotY = 1f
+ bubbleStashController.onNewBubbleAnimationInterrupted(
+ /* isStashed= */ bubbleBarView.alpha == 0f,
+ bubbleBarView.translationY
+ )
+ }
}
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 f46fdac..7065075 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
@@ -43,6 +43,7 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -52,47 +53,23 @@
class BubbleBarViewAnimatorTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
- private val animatorScheduler = TestBubbleBarViewAnimatorScheduler()
+ private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler
+ private lateinit var overflowView: BubbleView
+ private lateinit var bubbleView: BubbleView
+ private lateinit var bubble: BubbleBarBubble
+ private lateinit var bubbleBarView: BubbleBarView
+ private lateinit var bubbleStashController: BubbleStashController
@Before
fun setUp() {
+ animatorScheduler = TestBubbleBarViewAnimatorScheduler()
PhysicsAnimatorTestUtils.prepareForTest()
}
@Test
fun animateBubbleInForStashed() {
- lateinit var overflowView: BubbleView
- lateinit var bubbleView: BubbleView
- lateinit var bubble: BubbleBarBubble
- val bubbleBarView = BubbleBarView(context)
- InstrumentationRegistry.getInstrumentation().runOnMainSync {
- bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
- val inflater = LayoutInflater.from(context)
-
- val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
- overflowView =
- inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
- overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
- bubbleBarView.addView(overflowView)
-
- val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
- bubbleView =
- inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
- bubble =
- BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
- bubbleView.setBubble(bubble)
- bubbleBarView.addView(bubbleView)
- }
- InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-
- val bubbleStashController = mock<BubbleStashController>()
- whenever(bubbleStashController.isStashed).thenReturn(true)
- whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
- .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
- whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
- .thenReturn(HANDLE_TRANSLATION)
- whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
- .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ setUpBubbleBar()
+ setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
@@ -106,7 +83,7 @@
}
// let the animation start and wait for it to complete
- InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(handle.alpha).isEqualTo(0)
@@ -123,7 +100,7 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
// let the animation start and wait for it to complete
- InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(handle.alpha).isEqualTo(1)
@@ -135,38 +112,8 @@
@Test
fun animateBubbleInForStashed_tapAnimatingBubble() {
- lateinit var overflowView: BubbleView
- lateinit var bubbleView: BubbleView
- lateinit var bubble: BubbleBarBubble
- val bubbleBarView = BubbleBarView(context)
- InstrumentationRegistry.getInstrumentation().runOnMainSync {
- bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
- val inflater = LayoutInflater.from(context)
-
- val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
- overflowView =
- inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
- overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
- bubbleBarView.addView(overflowView)
-
- val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
- bubbleView =
- inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
- bubble =
- BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
- bubbleView.setBubble(bubble)
- bubbleBarView.addView(bubbleView)
- }
- InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-
- val bubbleStashController = mock<BubbleStashController>()
- whenever(bubbleStashController.isStashed).thenReturn(true)
- whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
- .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
- whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
- .thenReturn(HANDLE_TRANSLATION)
- whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
- .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ setUpBubbleBar()
+ setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
@@ -180,7 +127,7 @@
}
// let the animation start and wait for it to complete
- InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(handle.alpha).isEqualTo(0)
@@ -206,6 +153,151 @@
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
}
+ @Test
+ fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble)
+ }
+
+ // wait for the animation to start
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+ assertThat(handleAnimator.isRunning()).isTrue()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+ // verify the hide bubble animation is pending
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.onStashStateChangingWhileAnimating()
+ }
+
+ // verify that the hide animation was canceled
+ assertThat(animatorScheduler.delayedBlock).isNull()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
+
+ // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
+ // again
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ assertThat(handleAnimator.isRunning()).isFalse()
+ }
+
+ @Test
+ fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble)
+ }
+
+ // let the animation start and wait for it to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ // execute the hide bubble animation
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ // wait for the hide animation to start
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ assertThat(handleAnimator.isRunning()).isTrue()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.onStashStateChangingWhileAnimating()
+ }
+
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
+
+ // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
+ // again
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ assertThat(handleAnimator.isRunning()).isFalse()
+ }
+
+ @Test
+ fun animateBubbleInForStashed_showAnimationCanceled() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble)
+ }
+
+ // wait for the animation to start
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+ assertThat(handleAnimator.isRunning()).isTrue()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ handleAnimator.cancel()
+ assertThat(handleAnimator.isRunning()).isFalse()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ assertThat(animatorScheduler.delayedBlock).isNull()
+ }
+
+ private fun setUpBubbleBar() {
+ bubbleBarView = BubbleBarView(context)
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+ val inflater = LayoutInflater.from(context)
+
+ val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
+ overflowView =
+ inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+ overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
+ bubbleBarView.addView(overflowView)
+
+ val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
+ bubbleView =
+ inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+ bubble =
+ BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+ bubbleView.setBubble(bubble)
+ bubbleBarView.addView(bubbleView)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ }
+
+ private fun setUpBubbleStashController() {
+ bubbleStashController = mock<BubbleStashController>()
+ whenever(bubbleStashController.isStashed).thenReturn(true)
+ whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
+ .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
+ whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
+ .thenReturn(HANDLE_TRANSLATION)
+ whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ }
+
private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
var delayedBlock: Runnable? = null