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
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 2cdc0ce..83123b5 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 @@
/** 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 @@
}
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 @@
* 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 52f5a29..8158fe7 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 @@
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 0a0cfd0..b745193 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 @@
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 @@
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 @@
)
/** 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 @@
* 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 @@
// 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 @@
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 @@
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 @@
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 @@
*/
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 @@
/** 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 @@
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 @@
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 @@
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 @@
* 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 @@
)
}
+ 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 <T> PhysicsAnimator<T>?.cancelIfRunning() {
if (this?.isRunning() == true) cancel()
}
@@ -405,4 +486,37 @@
)
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 0f43744..48eb7de 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 @@
/** 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 62fe221..1b65019 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 @@
// 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 23e009b..1a4b982 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 @@
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 619ce1c..21eb3e0 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 @@
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 @@
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 @@
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 @@
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 @@
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.animateBubbleInForStashed(bubble)
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
}
// wait for the animation to start
@@ -270,6 +270,123 @@
}
@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()
setUpBubbleStashController()
@@ -336,17 +453,11 @@
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()
}
@@ -385,6 +496,79 @@
}
@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()
setUpBubbleStashController()
@@ -397,7 +581,7 @@
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.animateBubbleBarForCollapsed(bubble)
+ animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
}
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
@@ -424,6 +608,142 @@
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 @@
.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 <T> PhysicsAnimator<T>.assertIsRunning() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
assertThat(isRunning()).isTrue()