From 209c76417540d04a42619d18ef4a8564c3c64469 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Thu, 2 May 2024 15:15:46 -0400 Subject: [PATCH] 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 --- .../bubbles/BubbleBarViewController.java | 7 + .../bubbles/BubbleStashController.java | 30 +++ .../animation/BubbleBarViewAnimator.kt | 31 ++- .../animation/BubbleBarViewAnimatorTest.kt | 228 ++++++++++++------ 4 files changed, 225 insertions(+), 71 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 0b927488f3..715a4d0a21 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 @@ public class BubbleBarViewController { } } + /** 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 f689a05e7f..4b3416c0ce 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 @@ public class BubbleStashController { && !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 @@ public class BubbleStashController { 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 a6d0ff8a2b..66521c1117 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 @@ constructor( } } } - 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 @@ constructor( * 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 @@ constructor( } } } - animator.addEndListener { _, _, _, _, _, _, _ -> + animator.addEndListener { _, _, _, canceled, _, _, _ -> animatingBubble = null - bubbleStashController.stashBubbleBarImmediate() + if (!canceled) bubbleStashController.stashBubbleBarImmediate() bubbleBarView.onAnimatingBubbleCompleted() bubbleBarView.relativePivotY = 1f bubbleStashController.updateTaskbarTouchRegion() @@ -256,4 +267,18 @@ constructor( 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 f46fdac001..70650750c2 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 com.google.common.truth.Truth.assertThat 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 @@ import org.mockito.kotlin.whenever class BubbleBarViewAnimatorTest { private val context = ApplicationProvider.getApplicationContext() - 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() - 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 @@ class BubbleBarViewAnimatorTest { } // 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 @@ class BubbleBarViewAnimatorTest { 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 @@ class BubbleBarViewAnimatorTest { @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() - 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 @@ class BubbleBarViewAnimatorTest { } // 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 @@ class BubbleBarViewAnimatorTest { 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() + 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