Loading quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +172 −28 Original line number Diff line number Diff line Loading @@ -55,9 +55,11 @@ constructor( return animatingBubble.state != AnimatingBubble.State.CREATED } private var interceptedHandleAnimator = false private companion object { /** The time to show the flyout. */ const val FLYOUT_DELAY_MS: Long = 3000 const val FLYOUT_DELAY_MS: Long = 10000 /** The initial scale Y value that the new bubble is set to before the animation starts. */ const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f /** The minimum alpha value to make the bubble bar touchable. */ Loading Loading @@ -133,10 +135,21 @@ constructor( dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY, ) private fun cancelAnimationIfPending() { val animatingBubble = animatingBubble ?: return if (animatingBubble.state != AnimatingBubble.State.CREATED) return scheduler.cancel(animatingBubble.showAnimation) scheduler.cancel(animatingBubble.hideAnimation) } /** Animates a bubble for the state where the bubble bar is stashed. */ 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 if (isAnimating) { interruptAndUpdateAnimatingBubble(b.view, isExpanding) return } cancelAnimationIfPending() val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() Loading Loading @@ -165,9 +178,10 @@ 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 buildHandleToBubbleBarAnimation() = Runnable { private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable { moveToState(AnimatingBubble.State.ANIMATING_IN) // prepare the bubble bar for the animation // prepare the bubble bar for the animation if we're starting fresh if (initialVelocity == null) { bubbleBarView.visibility = VISIBLE bubbleBarView.alpha = 0f bubbleBarView.translationY = 0f Loading @@ -176,6 +190,7 @@ constructor( bubbleBarView.setBackgroundScaleX(1f) bubbleBarView.setBackgroundScaleY(1f) bubbleBarView.relativePivotY = 0.5f } // this is the offset between the center of the bubble bar and the center of the stash // handle. when the handle becomes invisible and we start animating in the bubble bar, Loading @@ -194,7 +209,7 @@ constructor( val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY) animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f) animator.addUpdateListener { handle, values -> val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener when { Loading Loading @@ -314,7 +329,19 @@ constructor( } } } animator.addEndListener { _, _, _, canceled, _, _, _ -> animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ -> // PhysicsAnimator calls the end listeners when the animation is replaced with a new one // if we're not in ANIMATING_OUT state, then this animation never started and we should // return if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener if (interceptedHandleAnimator) { interceptedHandleAnimator = false // post this to give a PhysicsAnimator a chance to clean up its internal listeners. // otherwise this end listener will be called as soon as we create a new spring // animation scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity)) return@addEndListener } animatingBubble = null if (!canceled) bubbleStashController.stashBubbleBarImmediate() bubbleBarView.relativePivotY = 1f Loading @@ -326,7 +353,7 @@ constructor( val flyout = bubble?.flyoutMessage if (flyout != null) { bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubble.view) onFlyoutRemoved() animator.start() } } else { Loading @@ -336,8 +363,6 @@ 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() Loading @@ -350,8 +375,11 @@ constructor( buildBubbleBarToHandleAnimation() } else { Runnable { bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) } moveToState(AnimatingBubble.State.ANIMATING_OUT) bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved() animatingBubble = null } bubbleStashController.showBubbleBarImmediate() bubbleStashController.updateTaskbarTouchRegion() } Loading Loading @@ -394,16 +422,23 @@ constructor( } 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 if (isAnimating) { interruptAndUpdateAnimatingBubble(b.view, isExpanding) return } cancelAnimationIfPending() val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() // first bounce the bubble bar and show the flyout. Then hide the flyout. val showAnimation = buildBubbleBarBounceAnimation() val hideAnimation = Runnable { bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) } moveToState(AnimatingBubble.State.ANIMATING_OUT) bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved() animatingBubble = null } bubbleStashController.showBubbleBarImmediate() bubbleStashController.updateTaskbarTouchRegion() } Loading Loading @@ -462,12 +497,11 @@ constructor( } private fun cancelFlyout() { val bubbleView = animatingBubble?.bubbleView bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved(bubbleView) } bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved() } } private fun onFlyoutRemoved(bubbleView: BubbleView?) { bubbleView?.suppressDotForBubbleUpdate(false) private fun onFlyoutRemoved() { animatingBubble?.bubbleView?.suppressDotForBubbleUpdate(false) bubbleStashController.updateTaskbarTouchRegion() } Loading Loading @@ -507,6 +541,116 @@ constructor( } } private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) { val animatingBubble = animatingBubble ?: return when (animatingBubble.state) { AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started AnimatingBubble.State.ANIMATING_IN -> updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding) AnimatingBubble.State.IN -> updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding) AnimatingBubble.State.ANIMATING_OUT -> updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding) } } private fun updateAnimationWhileAnimatingIn( animatingBubble: AnimatingBubble, bubbleView: BubbleView, isExpanding: Boolean, ) { this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) if (!bubbleBarFlyoutController.hasFlyout()) { // if the flyout does not yet exist, then we're only animating the bubble bar. // the animating bubble has been updated, so the when the flyout expands it will // show the right message. return } val bubble = bubbleView.bubble as? BubbleBarBubble val flyout = bubble?.flyoutMessage if (flyout != null) { // the flyout is currently expanding and we need to update it with new data bubbleView.suppressDotForBubbleUpdate(true) bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout) } else { // the flyout is expanding but we don't have new flyout data to update it with, // so cancel the expanding flyout. cancelFlyout() } } private fun updateAnimationWhileIn( animatingBubble: AnimatingBubble, bubbleView: BubbleView, isExpanding: Boolean, ) { // unsuppress the current bubble because we are about to hide its flyout animatingBubble.bubbleView.suppressDotForBubbleUpdate(false) this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) // we're currently idle, waiting for the hide animation to start. update the flyout // data and reschedule the hide animation to run later to give the user a chance to // see the new flyout. val hideAnimation = animatingBubble.hideAnimation scheduler.cancel(hideAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) val bubble = bubbleView.bubble as? BubbleBarBubble val flyout = bubble?.flyoutMessage if (flyout != null) { bubbleView.suppressDotForBubbleUpdate(true) bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) { bubbleStashController.updateTaskbarTouchRegion() } } else { cancelFlyout() } } private fun updateAnimationWhileAnimatingOut( animatingBubble: AnimatingBubble, bubbleView: BubbleView, isExpanding: Boolean, ) { // unsuppress the current bubble because we are about to hide its flyout animatingBubble.bubbleView.suppressDotForBubbleUpdate(false) this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) // the hide animation already started so it can't be canceled, just post it again val hideAnimation = animatingBubble.hideAnimation scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) val bubble = bubbleView.bubble as? BubbleBarBubble val flyout = bubble?.flyoutMessage if (bubbleBarFlyoutController.hasFlyout()) { // the flyout is collapsing. update it with the new flyout if (flyout != null) { moveToState(AnimatingBubble.State.ANIMATING_IN) bubbleView.suppressDotForBubbleUpdate(true) bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) { moveToState(AnimatingBubble.State.IN) bubbleStashController.updateTaskbarTouchRegion() } } else { cancelFlyout() moveToState(AnimatingBubble.State.IN) } } else { // the flyout is already gone. if we're animating the handle cancel it. the // animation itself can handle morphing back into the bubble bar and restarting // and show the flyout. val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator() if (handleAnimator != null && handleAnimator.isRunning()) { interceptedHandleAnimator = true handleAnimator.cancel() } // if we're not animating the handle, then the hide animation simply hides the // flyout, but if the flyout is gone then the animation has ended. } } private fun cancelHideAnimation() { val hideAnimation = animatingBubble?.hideAnimation ?: return scheduler.cancel(hideAnimation) Loading quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt +30 −8 Original line number Diff line number Diff line Loading @@ -39,11 +39,14 @@ constructor( } private var flyout: BubbleBarFlyoutView? = null private var animator: ValueAnimator? = null private val horizontalMargin = container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin) private enum class AnimationType { COLLAPSE, /** Morphs the flyout between a dot and a rounded rectangle. */ MORPH, /** Fades the flyout in or out. */ FADE, } Loading Loading @@ -73,16 +76,20 @@ constructor( container.addView(flyout, lp) this.flyout = flyout flyout.showFromCollapsed(message) { showFlyout(AnimationType.COLLAPSE, onEnd) } flyout.showFromCollapsed(message) { showFlyout(AnimationType.MORPH, onEnd) } } private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) { val flyout = this.flyout ?: return val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS) val startValue = getCurrentAnimatedValueIfRunning() ?: 0f val duration = (ANIMATION_DURATION_MS * (1f - startValue)).toLong() animator?.cancel() val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration) this.animator = animator when (animationType) { AnimationType.FADE -> animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float } AnimationType.COLLAPSE -> AnimationType.MORPH -> animator.addUpdateListener { _ -> flyout.updateExpansionProgress(animator.animatedValue as Float) } Loading @@ -109,6 +116,13 @@ constructor( flyout.updateData(message) { extendTopBoundary() } } fun updateFlyoutWhileCollapsing(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) { val flyout = flyout ?: return animator?.pause() animator?.removeAllListeners() flyout.updateData(message) { showFlyout(AnimationType.MORPH, onEnd) } } private fun extendTopBoundary() { val flyout = flyout ?: return val flyoutTop = flyout.top + flyout.translationY Loading @@ -125,20 +139,23 @@ constructor( } fun collapseFlyout(endAction: () -> Unit) { hideFlyout(AnimationType.COLLAPSE) { hideFlyout(AnimationType.MORPH) { cleanupFlyoutView() endAction() } } private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) { // TODO: b/277815200 - stop the current animation if it's running val flyout = this.flyout ?: return val animator = ValueAnimator.ofFloat(1f, 0f).setDuration(ANIMATION_DURATION_MS) val startValue = getCurrentAnimatedValueIfRunning() ?: 1f val duration = (ANIMATION_DURATION_MS * startValue).toLong() animator?.cancel() val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration) this.animator = animator when (animationType) { AnimationType.FADE -> animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float } AnimationType.COLLAPSE -> AnimationType.MORPH -> animator.addUpdateListener { _ -> flyout.updateExpansionProgress(animator.animatedValue as Float) } Loading @@ -154,4 +171,9 @@ constructor( } fun hasFlyout() = flyout != null private fun getCurrentAnimatedValueIfRunning(): Float? { val animator = animator ?: return null return if (animator.isRunning) animator.animatedValue as Float else null } } quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt +374 −14 File changed.Preview size limit exceeded, changes collapsed. Show changes quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt +34 −0 Original line number Diff line number Diff line Loading @@ -246,6 +246,40 @@ class BubbleBarFlyoutControllerTest { assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50) } @Test fun updateFlyoutWhileCollapsing() { InstrumentationRegistry.getInstrumentation().runOnMainSync { flyoutController.setUpAndShowFlyout(flyoutMessage) {} animatorTestRule.advanceTimeBy(300) } assertThat(flyoutController.hasFlyout()).isTrue() val newFlyoutMessage = flyoutMessage.copy(message = "new message") InstrumentationRegistry.getInstrumentation().runOnMainSync { var flyoutCollapsed = false flyoutController.collapseFlyout { flyoutCollapsed = true } // advance the fake timer so that the collapse animation runs for 125ms animatorTestRule.advanceTimeBy(125) // update the flyout in the middle of collapsing, which should start expanding it. var flyoutReversed = false flyoutController.updateFlyoutWhileCollapsing(newFlyoutMessage) { flyoutReversed = true } // the collapse animation ran for 125ms when it was updated, so reversing it should only // run for the same amount of time animatorTestRule.advanceTimeBy(125) val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view) assertThat(flyout.alpha).isEqualTo(1) assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text) .isEqualTo("new message") // verify that we never called the end action on the collapse animation assertThat(flyoutCollapsed).isFalse() // verify that we called the end action on the reverse animation assertThat(flyoutReversed).isTrue() } assertThat(flyoutController.hasFlyout()).isTrue() } class FakeFlyoutCallbacks : FlyoutCallbacks { var topBoundaryExtendedSpace = 0 Loading Loading
quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +172 −28 Original line number Diff line number Diff line Loading @@ -55,9 +55,11 @@ constructor( return animatingBubble.state != AnimatingBubble.State.CREATED } private var interceptedHandleAnimator = false private companion object { /** The time to show the flyout. */ const val FLYOUT_DELAY_MS: Long = 3000 const val FLYOUT_DELAY_MS: Long = 10000 /** The initial scale Y value that the new bubble is set to before the animation starts. */ const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f /** The minimum alpha value to make the bubble bar touchable. */ Loading Loading @@ -133,10 +135,21 @@ constructor( dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY, ) private fun cancelAnimationIfPending() { val animatingBubble = animatingBubble ?: return if (animatingBubble.state != AnimatingBubble.State.CREATED) return scheduler.cancel(animatingBubble.showAnimation) scheduler.cancel(animatingBubble.hideAnimation) } /** Animates a bubble for the state where the bubble bar is stashed. */ 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 if (isAnimating) { interruptAndUpdateAnimatingBubble(b.view, isExpanding) return } cancelAnimationIfPending() val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() Loading Loading @@ -165,9 +178,10 @@ 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 buildHandleToBubbleBarAnimation() = Runnable { private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable { moveToState(AnimatingBubble.State.ANIMATING_IN) // prepare the bubble bar for the animation // prepare the bubble bar for the animation if we're starting fresh if (initialVelocity == null) { bubbleBarView.visibility = VISIBLE bubbleBarView.alpha = 0f bubbleBarView.translationY = 0f Loading @@ -176,6 +190,7 @@ constructor( bubbleBarView.setBackgroundScaleX(1f) bubbleBarView.setBackgroundScaleY(1f) bubbleBarView.relativePivotY = 0.5f } // this is the offset between the center of the bubble bar and the center of the stash // handle. when the handle becomes invisible and we start animating in the bubble bar, Loading @@ -194,7 +209,7 @@ constructor( val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY) animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f) animator.addUpdateListener { handle, values -> val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener when { Loading Loading @@ -314,7 +329,19 @@ constructor( } } } animator.addEndListener { _, _, _, canceled, _, _, _ -> animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ -> // PhysicsAnimator calls the end listeners when the animation is replaced with a new one // if we're not in ANIMATING_OUT state, then this animation never started and we should // return if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener if (interceptedHandleAnimator) { interceptedHandleAnimator = false // post this to give a PhysicsAnimator a chance to clean up its internal listeners. // otherwise this end listener will be called as soon as we create a new spring // animation scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity)) return@addEndListener } animatingBubble = null if (!canceled) bubbleStashController.stashBubbleBarImmediate() bubbleBarView.relativePivotY = 1f Loading @@ -326,7 +353,7 @@ constructor( val flyout = bubble?.flyoutMessage if (flyout != null) { bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubble.view) onFlyoutRemoved() animator.start() } } else { Loading @@ -336,8 +363,6 @@ 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() Loading @@ -350,8 +375,11 @@ constructor( buildBubbleBarToHandleAnimation() } else { Runnable { bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) } moveToState(AnimatingBubble.State.ANIMATING_OUT) bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved() animatingBubble = null } bubbleStashController.showBubbleBarImmediate() bubbleStashController.updateTaskbarTouchRegion() } Loading Loading @@ -394,16 +422,23 @@ constructor( } 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 if (isAnimating) { interruptAndUpdateAnimatingBubble(b.view, isExpanding) return } cancelAnimationIfPending() val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() // first bounce the bubble bar and show the flyout. Then hide the flyout. val showAnimation = buildBubbleBarBounceAnimation() val hideAnimation = Runnable { bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) } moveToState(AnimatingBubble.State.ANIMATING_OUT) bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved() animatingBubble = null } bubbleStashController.showBubbleBarImmediate() bubbleStashController.updateTaskbarTouchRegion() } Loading Loading @@ -462,12 +497,11 @@ constructor( } private fun cancelFlyout() { val bubbleView = animatingBubble?.bubbleView bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved(bubbleView) } bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved() } } private fun onFlyoutRemoved(bubbleView: BubbleView?) { bubbleView?.suppressDotForBubbleUpdate(false) private fun onFlyoutRemoved() { animatingBubble?.bubbleView?.suppressDotForBubbleUpdate(false) bubbleStashController.updateTaskbarTouchRegion() } Loading Loading @@ -507,6 +541,116 @@ constructor( } } private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) { val animatingBubble = animatingBubble ?: return when (animatingBubble.state) { AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started AnimatingBubble.State.ANIMATING_IN -> updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding) AnimatingBubble.State.IN -> updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding) AnimatingBubble.State.ANIMATING_OUT -> updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding) } } private fun updateAnimationWhileAnimatingIn( animatingBubble: AnimatingBubble, bubbleView: BubbleView, isExpanding: Boolean, ) { this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) if (!bubbleBarFlyoutController.hasFlyout()) { // if the flyout does not yet exist, then we're only animating the bubble bar. // the animating bubble has been updated, so the when the flyout expands it will // show the right message. return } val bubble = bubbleView.bubble as? BubbleBarBubble val flyout = bubble?.flyoutMessage if (flyout != null) { // the flyout is currently expanding and we need to update it with new data bubbleView.suppressDotForBubbleUpdate(true) bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout) } else { // the flyout is expanding but we don't have new flyout data to update it with, // so cancel the expanding flyout. cancelFlyout() } } private fun updateAnimationWhileIn( animatingBubble: AnimatingBubble, bubbleView: BubbleView, isExpanding: Boolean, ) { // unsuppress the current bubble because we are about to hide its flyout animatingBubble.bubbleView.suppressDotForBubbleUpdate(false) this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) // we're currently idle, waiting for the hide animation to start. update the flyout // data and reschedule the hide animation to run later to give the user a chance to // see the new flyout. val hideAnimation = animatingBubble.hideAnimation scheduler.cancel(hideAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) val bubble = bubbleView.bubble as? BubbleBarBubble val flyout = bubble?.flyoutMessage if (flyout != null) { bubbleView.suppressDotForBubbleUpdate(true) bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) { bubbleStashController.updateTaskbarTouchRegion() } } else { cancelFlyout() } } private fun updateAnimationWhileAnimatingOut( animatingBubble: AnimatingBubble, bubbleView: BubbleView, isExpanding: Boolean, ) { // unsuppress the current bubble because we are about to hide its flyout animatingBubble.bubbleView.suppressDotForBubbleUpdate(false) this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) // the hide animation already started so it can't be canceled, just post it again val hideAnimation = animatingBubble.hideAnimation scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) val bubble = bubbleView.bubble as? BubbleBarBubble val flyout = bubble?.flyoutMessage if (bubbleBarFlyoutController.hasFlyout()) { // the flyout is collapsing. update it with the new flyout if (flyout != null) { moveToState(AnimatingBubble.State.ANIMATING_IN) bubbleView.suppressDotForBubbleUpdate(true) bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) { moveToState(AnimatingBubble.State.IN) bubbleStashController.updateTaskbarTouchRegion() } } else { cancelFlyout() moveToState(AnimatingBubble.State.IN) } } else { // the flyout is already gone. if we're animating the handle cancel it. the // animation itself can handle morphing back into the bubble bar and restarting // and show the flyout. val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator() if (handleAnimator != null && handleAnimator.isRunning()) { interceptedHandleAnimator = true handleAnimator.cancel() } // if we're not animating the handle, then the hide animation simply hides the // flyout, but if the flyout is gone then the animation has ended. } } private fun cancelHideAnimation() { val hideAnimation = animatingBubble?.hideAnimation ?: return scheduler.cancel(hideAnimation) Loading
quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt +30 −8 Original line number Diff line number Diff line Loading @@ -39,11 +39,14 @@ constructor( } private var flyout: BubbleBarFlyoutView? = null private var animator: ValueAnimator? = null private val horizontalMargin = container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin) private enum class AnimationType { COLLAPSE, /** Morphs the flyout between a dot and a rounded rectangle. */ MORPH, /** Fades the flyout in or out. */ FADE, } Loading Loading @@ -73,16 +76,20 @@ constructor( container.addView(flyout, lp) this.flyout = flyout flyout.showFromCollapsed(message) { showFlyout(AnimationType.COLLAPSE, onEnd) } flyout.showFromCollapsed(message) { showFlyout(AnimationType.MORPH, onEnd) } } private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) { val flyout = this.flyout ?: return val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS) val startValue = getCurrentAnimatedValueIfRunning() ?: 0f val duration = (ANIMATION_DURATION_MS * (1f - startValue)).toLong() animator?.cancel() val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration) this.animator = animator when (animationType) { AnimationType.FADE -> animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float } AnimationType.COLLAPSE -> AnimationType.MORPH -> animator.addUpdateListener { _ -> flyout.updateExpansionProgress(animator.animatedValue as Float) } Loading @@ -109,6 +116,13 @@ constructor( flyout.updateData(message) { extendTopBoundary() } } fun updateFlyoutWhileCollapsing(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) { val flyout = flyout ?: return animator?.pause() animator?.removeAllListeners() flyout.updateData(message) { showFlyout(AnimationType.MORPH, onEnd) } } private fun extendTopBoundary() { val flyout = flyout ?: return val flyoutTop = flyout.top + flyout.translationY Loading @@ -125,20 +139,23 @@ constructor( } fun collapseFlyout(endAction: () -> Unit) { hideFlyout(AnimationType.COLLAPSE) { hideFlyout(AnimationType.MORPH) { cleanupFlyoutView() endAction() } } private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) { // TODO: b/277815200 - stop the current animation if it's running val flyout = this.flyout ?: return val animator = ValueAnimator.ofFloat(1f, 0f).setDuration(ANIMATION_DURATION_MS) val startValue = getCurrentAnimatedValueIfRunning() ?: 1f val duration = (ANIMATION_DURATION_MS * startValue).toLong() animator?.cancel() val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration) this.animator = animator when (animationType) { AnimationType.FADE -> animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float } AnimationType.COLLAPSE -> AnimationType.MORPH -> animator.addUpdateListener { _ -> flyout.updateExpansionProgress(animator.animatedValue as Float) } Loading @@ -154,4 +171,9 @@ constructor( } fun hasFlyout() = flyout != null private fun getCurrentAnimatedValueIfRunning(): Float? { val animator = animator ?: return null return if (animator.isRunning) animator.animatedValue as Float else null } }
quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt +374 −14 File changed.Preview size limit exceeded, changes collapsed. Show changes
quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt +34 −0 Original line number Diff line number Diff line Loading @@ -246,6 +246,40 @@ class BubbleBarFlyoutControllerTest { assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50) } @Test fun updateFlyoutWhileCollapsing() { InstrumentationRegistry.getInstrumentation().runOnMainSync { flyoutController.setUpAndShowFlyout(flyoutMessage) {} animatorTestRule.advanceTimeBy(300) } assertThat(flyoutController.hasFlyout()).isTrue() val newFlyoutMessage = flyoutMessage.copy(message = "new message") InstrumentationRegistry.getInstrumentation().runOnMainSync { var flyoutCollapsed = false flyoutController.collapseFlyout { flyoutCollapsed = true } // advance the fake timer so that the collapse animation runs for 125ms animatorTestRule.advanceTimeBy(125) // update the flyout in the middle of collapsing, which should start expanding it. var flyoutReversed = false flyoutController.updateFlyoutWhileCollapsing(newFlyoutMessage) { flyoutReversed = true } // the collapse animation ran for 125ms when it was updated, so reversing it should only // run for the same amount of time animatorTestRule.advanceTimeBy(125) val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view) assertThat(flyout.alpha).isEqualTo(1) assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text) .isEqualTo("new message") // verify that we never called the end action on the collapse animation assertThat(flyoutCollapsed).isFalse() // verify that we called the end action on the reverse animation assertThat(flyoutReversed).isTrue() } assertThat(flyoutController.hasFlyout()).isTrue() } class FakeFlyoutCallbacks : FlyoutCallbacks { var topBoundaryExtendedSpace = 0 Loading