diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index 66e530294c..8c83508ad9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -403,9 +403,6 @@ public class BubbleBarController extends IBubblesListener.Stub { } if (bubbleToSelect != null) { setSelectedBubbleInternal(bubbleToSelect); - if (previouslySelectedBubble == null) { - mBubbleStashController.animateToInitialState(update.expanded); - } } if (update.shouldShowEducation) { mBubbleBarViewController.prepareToShowEducation(); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 0b74e15d27..ac02f1fe5e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -406,10 +406,21 @@ public class BubbleBarViewController { return; } + if (!(b instanceof BubbleBarBubble bubble)) { + return; + } + boolean isInApp = mTaskbarStashController.isInApp(); + // if this is the first bubble, animate to the initial state. one bubble is the overflow + // so check for at most 2 children. + if (mBarView.getChildCount() <= 2) { + mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding); + return; + } + // only animate the new bubble if we're in an app and not auto expanding - if (b instanceof BubbleBarBubble && isInApp && !isExpanding && !isExpanded()) { - mBubbleBarViewAnimator.animateBubbleInForStashed((BubbleBarBubble) b); + if (isInApp && !isExpanding && !isExpanded()) { + mBubbleBarViewAnimator.animateBubbleInForStashed(bubble); } } else { Log.w(TAG, "addBubble, bubble was null!"); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java index 4b3416c0ce..d0462aa3da 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java @@ -123,21 +123,17 @@ public class BubbleStashController { } /** - * Animates the bubble bar and handle to their initial state, transitioning from the state where - * both views are invisible. Called when the first bubble is added or when the device is + * Animates the handle (or bubble bar depending on state) to be visible after the device is * unlocked. * *

Normally either the bubble bar or the handle is visible, * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition * between these two states. But the transition from the state where both the bar and handle * are invisible is slightly different. - * - *

The initial state will depend on the current state of the device, i.e. overview, home etc - * and whether bubbles are requested to be expanded. */ - public void animateToInitialState(boolean expanding) { + private void animateAfterUnlock() { AnimatorSet animatorSet = new AnimatorSet(); - if (expanding || mBubblesShowingOnHome || mBubblesShowingOnOverview) { + if (mBubblesShowingOnHome || mBubblesShowingOnOverview) { mIsStashed = false; animatorSet.playTogether(mIconScaleForStash.animateToValue(1), mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()), @@ -217,7 +213,7 @@ public class BubbleStashController { if (isSysuiLocked != mIsSysuiLocked) { mIsSysuiLocked = isSysuiLocked; if (!mIsSysuiLocked && mBarViewController.hasBubbles()) { - animateToInitialState(false /* expanding */); + animateAfterUnlock(); } } } @@ -453,4 +449,9 @@ public class BubbleStashController { mIsStashed = isStashed; onIsStashedChanged(); } + + /** Set the translation Y for the stashed handle. */ + public void setHandleTranslationY(float ty) { + mHandleViewController.setTranslationYForSwipe(ty); + } } 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 66521c1117..be935d8b54 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -93,15 +93,15 @@ constructor( 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 = buildShowAnimation() - val hideAnimation = buildHideAnimation() + val showAnimation = buildHandleToBubbleBarAnimation() + val hideAnimation = buildBubbleBarToHandleAnimation() animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } /** - * Returns a [Runnable] that starts the animation that shows the new or updated bubble. + * Returns a [Runnable] that starts the animation that morphs the handle to the bubble bar. * * Visually, the animation is divided into 2 parts. The stash handle starts animating up and * fading out and then the bubble bar starts animating up and fading in. @@ -114,7 +114,7 @@ constructor( * 3. The third part is the overshoot of the spring animation, where we make the bubble fully * visible which helps avoiding further updates when we re-enter the second part. */ - private fun buildShowAnimation() = Runnable { + private fun buildHandleToBubbleBarAnimation() = Runnable { // prepare the bubble bar for the animation bubbleBarView.onAnimatingBubbleStarted() bubbleBarView.visibility = VISIBLE @@ -197,7 +197,8 @@ constructor( } /** - * Returns a [Runnable] that starts the animation that hides the bubble bar. + * Returns a [Runnable] that starts the animation that hides the bubble bar and morphs it into + * the stashed handle. * * Similarly to the show animation, this is visually divided into 2 parts. We first animate the * bubble bar out, and then animate the stash handle in. At the end of the animation we reset @@ -209,13 +210,14 @@ constructor( * 2. In the second part the bubble bar is fully hidden and the handle animates in. * 3. The third part is the overshoot. The handle is made fully visible. */ - private fun buildHideAnimation() = Runnable { + private fun buildBubbleBarToHandleAnimation() = Runnable { if (animatingBubble == null) return@Runnable val offset = bubbleStashController.diffBetweenHandleAndBarCenters val stashedHandleTranslationY = bubbleStashController.stashedHandleTranslationForNewBubbleAnimation // this is the total distance that both the stashed handle and the bar will be traveling val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset + bubbleStashController.setHandleTranslationY(totalTranslationY) val animator = bubbleStashController.stashedHandlePhysicsAnimator animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, 0f) @@ -259,6 +261,50 @@ constructor( animator.start() } + /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */ + fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) { + 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 if we are in an app. + val showAnimation = buildBubbleBarBounceAnimation() + val hideAnimation = + if (isInApp && !isExpanding) { + buildBubbleBarToHandleAnimation() + } else { + // in this case the bubble bar remains visible so not much to do. once we implement + // the flyout we'll update this runnable to hide it. + Runnable { + animatingBubble = null + bubbleStashController.showBubbleBarImmediate() + bubbleBarView.onAnimatingBubbleCompleted() + } + } + animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) + scheduler.post(showAnimation) + scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) + } + + private fun buildBubbleBarBounceAnimation() = Runnable { + // prepare the bubble bar for the animation + bubbleBarView.onAnimatingBubbleStarted() + bubbleBarView.translationY = bubbleBarView.height.toFloat() + bubbleBarView.visibility = VISIBLE + bubbleBarView.alpha = 1f + bubbleBarView.scaleX = 1f + bubbleBarView.scaleY = 1f + + val animator = PhysicsAnimator.getInstance(bubbleBarView) + animator.setDefaultSpringConfig(springConfig) + animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY) + animator.addEndListener { _, _, _, _, _, _, _ -> + // the bubble bar is now fully settled in. update taskbar touch region so it's touchable + bubbleStashController.updateTaskbarTouchRegion() + } + animator.start() + } + /** Handles clicking on the animating bubble while the animation is still playing. */ fun onBubbleClickedWhileAnimating() { val hideAnimation = animatingBubble?.hideAnimation ?: return 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 70650750c2..2bcfa3f224 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 @@ -264,6 +264,120 @@ class BubbleBarViewAnimatorTest { assertThat(animatorScheduler.delayedBlock).isNull() } + @Test + fun animateToInitialState_inApp() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + val animator = + BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = true, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(barAnimator.isRunning()).isFalse() + 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!!) + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(handle.translationY).isEqualTo(0) + assertThat(handle.alpha).isEqualTo(1) + + verify(bubbleStashController).stashBubbleBarImmediate() + } + + @Test + fun animateToInitialState_inApp_autoExpanding() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + val animator = + BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = true, isExpanding = true) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(barAnimator.isRunning()).isFalse() + 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) + + verify(bubbleStashController).showBubbleBarImmediate() + } + + @Test + fun animateToInitialState_inHome() { + 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.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(barAnimator.isRunning()).isFalse() + assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + 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_HOTSEAT) + + verify(bubbleStashController).showBubbleBarImmediate() + } + private fun setUpBubbleBar() { bubbleBarView = BubbleBarView(context) InstrumentationRegistry.getInstrumentation().runOnMainSync { @@ -322,3 +436,4 @@ class BubbleBarViewAnimatorTest { private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f private const val HANDLE_TRANSLATION = -30f private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f +private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f