From 46111b109e03b2b15ff9d6e0d692ae1fd3fd5d64 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Tue, 6 Aug 2024 08:59:26 -0400 Subject: [PATCH] Support expanding the bar while animating Update the BubbleBarAnimator to handle auto expanding bubbles as well as expand signals that are received while animating the bubble bar. The expansion starts after the bar reaches its peak height. If the hide animation starts before the expansion signal is received, we ignore it. Next step is to delay showing the expanded view until the bubble bar starts expanding. Flag: com.android.wm.shell.enable_bubble_bar Fixes: 353644484 Fixes: 356415377 Bug: 339683389 Test: atest BubbleBarViewAnimatorTest Change-Id: I79a63f0b8728abc1ab3345f0116cbfcba2918643 --- .../bubbles/BubbleBarViewController.java | 15 +- .../BubbleStashedHandleViewController.java | 5 + .../animation/BubbleBarViewAnimator.kt | 164 ++++++-- .../bubbles/stashing/BubbleStashController.kt | 3 + .../PersistentBubbleStashController.kt | 2 + .../TransientBubbleStashController.kt | 2 + .../animation/BubbleBarViewAnimatorTest.kt | 354 +++++++++++++++++- 7 files changed, 502 insertions(+), 43 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 2cdc0ced86..83123b507b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -303,7 +303,8 @@ public class BubbleBarViewController { /** Whether a new bubble is animating. */ public boolean isAnimatingNewBubble() { - return mBarView.isAnimatingNewBubble(); + return mBarView.isAnimatingNewBubble() + || (mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimatingBubble()); } /** The horizontal margin of the bubble bar from the edge of the screen. */ @@ -575,14 +576,14 @@ public class BubbleBarViewController { } boolean persistentTaskbarOrOnHome = mBubbleStashController.isBubblesShowingOnHome() || !mBubbleStashController.isTransientTaskBar(); - if (persistentTaskbarOrOnHome && !isExpanding && !isExpanded()) { - mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble); + if (persistentTaskbarOrOnHome && !isExpanded()) { + mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding); return; } // only animate the new bubble if we're in an app, have handle view and not auto expanding - if (isInApp && !isExpanding && mBubbleStashController.getHasHandleView() && !isExpanded()) { - mBubbleBarViewAnimator.animateBubbleInForStashed(bubble); + if (isInApp && mBubbleStashController.getHasHandleView() && !isExpanded()) { + mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding); } } @@ -626,6 +627,10 @@ public class BubbleBarViewController { * from SystemUI. */ public void setExpandedFromSysui(boolean isExpanded) { + if (isAnimatingNewBubble() && isExpanded) { + mBubbleBarViewAnimator.expandedWhileAnimating(); + return; + } if (!isExpanded) { mBubbleStashController.stashBubbleBar(); } else { diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java index 52f5a29401..8158fe79b4 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java @@ -245,6 +245,11 @@ public class BubbleStashedHandleViewController { mStashedHandleView.setTranslationY(transY); } + /** Returns the translation of the stashed handle. */ + public float getTranslationY() { + return mStashedHandleView.getTranslationY(); + } + /** * Used by {@link BubbleStashController} to animate the handle when stashing or un stashing. */ 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 0a0cfd007b..b745193e2b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -43,6 +43,8 @@ constructor( private val bubbleBarBounceDistanceInPx = bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance) + fun hasAnimatingBubble() = animatingBubble != null + private companion object { /** The time to show the flyout. */ const val FLYOUT_DELAY_MS: Long = 2500 @@ -58,8 +60,33 @@ constructor( private data class AnimatingBubble( val bubbleView: BubbleView, val showAnimation: Runnable, - val hideAnimation: Runnable - ) + val hideAnimation: Runnable, + val expand: Boolean, + val state: State = State.CREATED + ) { + + /** + * The state of the animation. + * + * The animation is initially created but will be scheduled later using the [Scheduler]. + * + * The normal uninterrupted cycle is for the bubble notification to animate in, then be in a + * transient state and eventually to animate out. + * + * However different events, such as touch and external signals, may cause the animation to + * end earlier. + */ + enum class State { + /** The animation is created but not started yet. */ + CREATED, + /** The bubble notification is animating in. */ + ANIMATING_IN, + /** The bubble notification is now fully showing and waiting to be hidden. */ + IN, + /** The bubble notification is animating out. */ + ANIMATING_OUT + } + } /** An interface for scheduling jobs. */ interface Scheduler { @@ -97,15 +124,18 @@ constructor( ) /** Animates a bubble for the state where the bubble bar is stashed. */ - fun animateBubbleInForStashed(b: BubbleBarBubble) { + fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) { + // TODO b/346400677: handle animations for the same bubble interrupting each other + if (animatingBubble?.bubbleView?.bubble?.key == b.key) return 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. val showAnimation = buildHandleToBubbleBarAnimation() - val hideAnimation = buildBubbleBarToHandleAnimation() - animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) + val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation() + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } @@ -125,6 +155,7 @@ constructor( * visible which helps avoiding further updates when we re-enter the second part. */ private fun buildHandleToBubbleBarAnimation() = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) // prepare the bubble bar for the animation bubbleBarView.onAnimatingBubbleStarted() bubbleBarView.visibility = VISIBLE @@ -138,9 +169,12 @@ constructor( // handle. when the handle becomes invisible and we start animating in the bubble bar, // the translation y is offset by this value to make the transition from the handle to the // bar smooth. - val offset: Float = bubbleStashController.getDiffBetweenHandleAndBarCenters() - val stashedHandleTranslationY: Float = + val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() + val stashedHandleTranslationYForAnimation = bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() + val stashedHandleTranslationY = + bubbleStashController.getHandleTranslationY() ?: return@Runnable + val translationTracker = TranslationTracker(stashedHandleTranslationY) // this is the total distance that both the stashed handle and the bubble will be traveling // at the end of the animation the bubble bar will be positioned in the same place when it @@ -150,15 +184,14 @@ constructor( animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY) animator.addUpdateListener { handle, values -> - val ty: Float = - values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener + val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener when { - ty >= stashedHandleTranslationY -> { + ty >= stashedHandleTranslationYForAnimation -> { // we're in the first leg of the animation. only animate the handle. the bubble // bar remains hidden during this part of the animation // map the path [0, stashedHandleTranslationY] to [0,1] - val fraction = ty / stashedHandleTranslationY + val fraction = ty / stashedHandleTranslationYForAnimation handle.alpha = 1 - fraction } ty >= totalTranslationY -> { @@ -172,8 +205,8 @@ constructor( if (bubbleBarView.alpha != 1f) { // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1] val fraction = - (ty - stashedHandleTranslationY) / - (totalTranslationY - stashedHandleTranslationY) + (ty - stashedHandleTranslationYForAnimation) / + (totalTranslationY - stashedHandleTranslationYForAnimation) bubbleBarView.alpha = fraction bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y + @@ -193,18 +226,16 @@ constructor( bubbleStashController.updateTaskbarTouchRegion() } } + translationTracker.updateTyAndExpandIfNeeded(ty) } 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 + if (canceled || animatingBubble?.expand == true) { + cancelHideAnimation() return@addEndListener } + moveToState(AnimatingBubble.State.IN) // the bubble bar is now fully settled in. update taskbar touch region so it's touchable bubbleStashController.updateTaskbarTouchRegion() } @@ -227,7 +258,8 @@ constructor( */ private fun buildBubbleBarToHandleAnimation() = Runnable { if (animatingBubble == null) return@Runnable - val offset = bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() + moveToState(AnimatingBubble.State.ANIMATING_OUT) + val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() val stashedHandleTranslationY = bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() // this is the total distance that both the stashed handle and the bar will be traveling @@ -281,6 +313,8 @@ constructor( /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */ fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) { + // TODO b/346400677: handle animations for the same bubble interrupting each other + if (animatingBubble?.bubbleView?.bubble?.key == b.key) return val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() @@ -300,12 +334,14 @@ constructor( bubbleStashController.updateTaskbarTouchRegion() } } - animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } private fun buildBubbleBarSpringInAnimation() = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) // prepare the bubble bar for the animation bubbleBarView.onAnimatingBubbleStarted() bubbleBarView.translationY = bubbleBarView.height.toFloat() @@ -314,18 +350,31 @@ constructor( bubbleBarView.scaleX = 1f bubbleBarView.scaleY = 1f + val translationTracker = TranslationTracker(bubbleBarView.translationY) + val animator = PhysicsAnimator.getInstance(bubbleBarView) animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY) - animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() } + animator.addUpdateListener { _, values -> + val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener + translationTracker.updateTyAndExpandIfNeeded(ty) + bubbleStashController.updateTaskbarTouchRegion() + } animator.addEndListener { _, _, _, _, _, _, _ -> + if (animatingBubble?.expand == true) { + cancelHideAnimation() + } else { + moveToState(AnimatingBubble.State.IN) + } // the bubble bar is now fully settled in. update taskbar touch region so it's touchable bubbleStashController.updateTaskbarTouchRegion() } animator.start() } - fun animateBubbleBarForCollapsed(b: BubbleBarBubble) { + fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) { + // TODO b/346400677: handle animations for the same bubble interrupting each other + if (animatingBubble?.bubbleView?.bubble?.key == b.key) return val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() @@ -336,7 +385,8 @@ constructor( bubbleBarView.onAnimatingBubbleCompleted() bubbleStashController.updateTaskbarTouchRegion() } - animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } @@ -347,17 +397,29 @@ constructor( * the bubble bar moves back to its initial position with a spring animation. */ private fun buildBubbleBarBounceAnimation() = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) bubbleBarView.onAnimatingBubbleStarted() val ty = bubbleStashController.bubbleBarTranslationY val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView) springBackAnimation.setDefaultSpringConfig(springConfig) springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty) + springBackAnimation.addEndListener { _, _, _, _, _, _, _ -> + if (animatingBubble?.expand == true) { + bubbleBarView.isExpanded = true + cancelHideAnimation() + } else { + moveToState(AnimatingBubble.State.IN) + } + } // 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() } + .withEndAction { + if (animatingBubble?.expand == true) bubbleBarView.isExpanded = true + springBackAnimation.start() + } .start() } @@ -386,6 +448,25 @@ constructor( ) } + fun expandedWhileAnimating() { + val animatingBubble = animatingBubble ?: return + this.animatingBubble = animatingBubble.copy(expand = true) + // if we're fully in and waiting to hide, cancel the hide animation and clean up + if (animatingBubble.state == AnimatingBubble.State.IN) { + bubbleBarView.isExpanded = true + cancelHideAnimation() + } + } + + private fun cancelHideAnimation() { + val hideAnimation = animatingBubble?.hideAnimation ?: return + scheduler.cancel(hideAnimation) + animatingBubble = null + bubbleBarView.onAnimatingBubbleCompleted() + bubbleBarView.relativePivotY = 1f + bubbleStashController.showBubbleBarImmediate() + } + private fun PhysicsAnimator?.cancelIfRunning() { if (this?.isRunning() == true) cancel() } @@ -405,4 +486,37 @@ constructor( ) return this } + + private fun moveToState(state: AnimatingBubble.State) { + val animatingBubble = this.animatingBubble ?: return + this.animatingBubble = animatingBubble.copy(state = state) + } + + /** + * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands + * as part of the animation, the expansion should start after the bubble bar reaches the peak + * position. + */ + private inner class TranslationTracker(initialTy: Float) { + private var previousTy = initialTy + private var startedExpanding = false + private var reachedPeak = false + + fun updateTyAndExpandIfNeeded(ty: Float) { + if (!reachedPeak) { + // the bubble bar is positioned at the bottom of the screen and moves up using + // negative ty values. the peak is reached the first time we see a value that is + // greater than the previous. + if (ty > previousTy) { + reachedPeak = true + } + } + val expand = animatingBubble?.expand ?: false + if (reachedPeak && expand && !startedExpanding) { + bubbleBarView.isExpanded = true + startedExpanding = true + } + previousTy = ty + } + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt index 0f437449bb..48eb7dea15 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt @@ -143,6 +143,9 @@ interface BubbleStashController { /** Set the translation Y for the stashed handle. */ fun setHandleTranslationY(translationY: Float) + /** Returns the translation of the handle. */ + fun getHandleTranslationY(): Float? + /** * Returns bubble bar Y position according to [isBubblesShowingOnHome] and * [isBubblesShowingOnOverview] values. Default implementation only analyse diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt index 62fe221c70..1b650195ef 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt @@ -198,6 +198,8 @@ class PersistentBubbleStashController( // no op since does not have a handle view } + override fun getHandleTranslationY(): Float? = null + private fun updateExpandedState(expand: Boolean) { if (bubbleBarViewController.isHiddenForNoBubbles) { // If there are no bubbles the bar is invisible, nothing to do here. diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt index 23e009b3a1..1a4b9823ef 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt @@ -242,6 +242,8 @@ class TransientBubbleStashController( bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY) } + override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY + private fun getStashTranslation(): Float { return (bubbleBarViewController.bubbleBarCollapsedHeight - stashedHeight) / 2f } 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 619ce1c860..21eb3e0f7d 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 @@ -84,7 +84,7 @@ class BubbleBarViewAnimatorTest { BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete @@ -128,7 +128,7 @@ class BubbleBarViewAnimatorTest { BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete @@ -171,7 +171,7 @@ class BubbleBarViewAnimatorTest { BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // wait for the animation to start @@ -211,7 +211,7 @@ class BubbleBarViewAnimatorTest { BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete @@ -252,7 +252,7 @@ class BubbleBarViewAnimatorTest { BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // wait for the animation to start @@ -269,6 +269,123 @@ class BubbleBarViewAnimatorTest { assertThat(animatorScheduler.delayedBlock).isNull() } + @Test + fun animateBubbleInForStashed_autoExpanding() { + setUpBubbleBar() + setUpBubbleStashController() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) + + val animator = + BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = true) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(bubbleBarView.isExpanded).isTrue() + + // verify there is no hide animation + assertThat(animatorScheduler.delayedBlock).isNull() + + verify(bubbleStashController).showBubbleBarImmediate() + } + + @Test + fun animateBubbleInForStashed_expandedWhileAnimatingIn() { + setUpBubbleBar() + setUpBubbleStashController() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) + + val animator = + BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the animation finish + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + } + + @Test + fun animateBubbleInForStashed_expandedWhileFullyIn() { + setUpBubbleBar() + setUpBubbleStashController() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) + + val animator = + BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // wait for the animation to end + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + } + @Test fun animateToInitialState_inApp() { setUpBubbleBar() @@ -336,17 +453,11 @@ class BubbleBarViewAnimatorTest { PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) barAnimator.assertIsNotRunning() - 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) + assertThat(animatorScheduler.delayedBlock).isNull() verify(bubbleStashController).showBubbleBarImmediate() } @@ -384,6 +495,79 @@ class BubbleBarViewAnimatorTest { verify(bubbleStashController).showBubbleBarImmediate() } + @Test + fun animateToInitialState_expandedWhileAnimatingIn() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + val animator = + BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + val bubbleBarAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(bubbleBarAnimator) { true } + + bubbleBarAnimator.assertIsRunning() + assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the animation finish + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + verify(bubbleStashController).showBubbleBarImmediate() + } + + @Test + fun animateToInitialState_expandedWhileFullyIn() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + val animator = + BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + } + @Test fun animateBubbleBarForCollapsed() { setUpBubbleBar() @@ -397,7 +581,7 @@ class BubbleBarViewAnimatorTest { BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleBarForCollapsed(bubble) + animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} @@ -424,6 +608,142 @@ class BubbleBarViewAnimatorTest { verify(bubbleStashController).showBubbleBarImmediate() } + @Test + fun animateBubbleBarForCollapsed_autoExpanding() { + 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.animateBubbleBarForCollapsed(bubble, isExpanding = true) + } + + 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) + + // verify there is no hide animation + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isExpanded).isTrue() + verify(bubbleStashController).showBubbleBarImmediate() + } + + @Test + fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() { + 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.animateBubbleBarForCollapsed(bubble, isExpanding = false) + } + + 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.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true } + + // verify there is a pending hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the animation finish + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isExpanded).isTrue() + verify(bubbleStashController).showBubbleBarImmediate() + } + + @Test + fun animateBubbleBarForCollapsed_expandingWhileFullyIn() { + 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.animateBubbleBarForCollapsed(bubble, isExpanding = false) + } + + 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) + + // verify there is a pending hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isExpanded).isTrue() + verify(bubbleStashController).showBubbleBarImmediate() + } + private fun setUpBubbleBar() { bubbleBarView = BubbleBarView(context) InstrumentationRegistry.getInstrumentation().runOnMainSync { @@ -459,6 +779,14 @@ class BubbleBarViewAnimatorTest { .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) } + private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) { + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(ty) + assertThat(bubbleBarView.isExpanded).isTrue() + } + private fun PhysicsAnimator.assertIsRunning() { InstrumentationRegistry.getInstrumentation().runOnMainSync { assertThat(isRunning()).isTrue()