1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.taskbar.bubbles.animation 18 19 import android.view.View 20 import android.view.View.VISIBLE 21 import androidx.core.animation.Animator 22 import androidx.core.animation.AnimatorListenerAdapter 23 import androidx.core.animation.ObjectAnimator 24 import androidx.dynamicanimation.animation.DynamicAnimation 25 import androidx.dynamicanimation.animation.SpringForce 26 import com.android.launcher3.R 27 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble 28 import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier 29 import com.android.launcher3.taskbar.bubbles.BubbleBarView 30 import com.android.launcher3.taskbar.bubbles.BubbleView 31 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController 32 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage 33 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController 34 import com.android.wm.shell.shared.animation.PhysicsAnimator 35 36 /** Handles animations for bubble bar bubbles. */ 37 class BubbleBarViewAnimator 38 @JvmOverloads 39 constructor( 40 private val bubbleBarView: BubbleBarView, 41 private val bubbleStashController: BubbleStashController, 42 private val bubbleBarFlyoutController: BubbleBarFlyoutController, 43 private val bubbleBarParentViewHeightUpdateNotifier: BubbleBarParentViewHeightUpdateNotifier, 44 private val onExpanded: Runnable, 45 private val onBubbleBarVisible: Runnable, 46 private val scheduler: Scheduler = HandlerScheduler(bubbleBarView), 47 ) { 48 49 private var animatingBubble: AnimatingBubble? = null 50 private val bubbleBarBounceDistanceInPx = 51 bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance) 52 53 fun hasAnimation() = animatingBubble != null 54 55 val isAnimating: Boolean 56 get() { 57 val animatingBubble = animatingBubble ?: return false 58 return animatingBubble.state != AnimatingBubble.State.CREATED 59 } 60 61 private var interceptedHandleAnimator = false 62 63 private companion object { 64 /** The time to show the flyout. */ 65 const val FLYOUT_DELAY_MS: Long = 3000 66 /** The initial scale Y value that the new bubble is set to before the animation starts. */ 67 const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f 68 /** The minimum alpha value to make the bubble bar touchable. */ 69 const val MIN_ALPHA_FOR_TOUCHABLE = 0.5f 70 /** The duration of the bounce animation. */ 71 const val BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS = 250L 72 } 73 74 /** Wrapper around the animating bubble with its show and hide animations. */ 75 private data class AnimatingBubble( 76 val bubbleView: BubbleView, 77 val showAnimation: Runnable, 78 val hideAnimation: Runnable, 79 val expand: Boolean, 80 val state: State = State.CREATED, 81 ) { 82 83 /** 84 * The state of the animation. 85 * 86 * The animation is initially created but will be scheduled later using the [Scheduler]. 87 * 88 * The normal uninterrupted cycle is for the bubble notification to animate in, then be in a 89 * transient state and eventually to animate out. 90 * 91 * However different events, such as touch and external signals, may cause the animation to 92 * end earlier. 93 */ 94 enum class State { 95 /** The animation is created but not started yet. */ 96 CREATED, 97 /** The bubble notification is animating in. */ 98 ANIMATING_IN, 99 /** The bubble notification is now fully showing and waiting to be hidden. */ 100 IN, 101 /** The bubble notification is animating out. */ 102 ANIMATING_OUT, 103 } 104 } 105 106 /** An interface for scheduling jobs. */ 107 interface Scheduler { 108 109 /** Schedule the given [block] to run. */ 110 fun post(block: Runnable) 111 112 /** Schedule the given [block] to start with a delay of [delayMillis]. */ 113 fun postDelayed(delayMillis: Long, block: Runnable) 114 115 /** Cancel the given [block] if it hasn't started yet. */ 116 fun cancel(block: Runnable) 117 } 118 119 /** A [Scheduler] that uses a Handler to run jobs. */ 120 private class HandlerScheduler(private val view: View) : Scheduler { 121 122 override fun post(block: Runnable) { 123 view.post(block) 124 } 125 126 override fun postDelayed(delayMillis: Long, block: Runnable) { 127 view.postDelayed(block, delayMillis) 128 } 129 130 override fun cancel(block: Runnable) { 131 view.removeCallbacks(block) 132 } 133 } 134 135 private val springConfig = 136 PhysicsAnimator.SpringConfig( 137 stiffness = SpringForce.STIFFNESS_LOW, 138 dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY, 139 ) 140 141 private fun cancelAnimationIfPending() { 142 val animatingBubble = animatingBubble ?: return 143 if (animatingBubble.state != AnimatingBubble.State.CREATED) return 144 scheduler.cancel(animatingBubble.showAnimation) 145 scheduler.cancel(animatingBubble.hideAnimation) 146 } 147 148 /** Animates a bubble for the state where the bubble bar is stashed. */ 149 fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) { 150 if (isAnimating) { 151 interruptAndUpdateAnimatingBubble(b.view, isExpanding) 152 return 153 } 154 cancelAnimationIfPending() 155 156 val bubbleView = b.view 157 val animator = PhysicsAnimator.getInstance(bubbleView) 158 if (animator.isRunning()) animator.cancel() 159 // the animation of a new bubble is divided into 2 parts. The first part transforms the 160 // handle to the bubble bar and then shows the flyout. The second part hides the flyout and 161 // transforms the bubble bar back to the handle. 162 val showAnimation = buildHandleToBubbleBarAnimation() 163 val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation() 164 animatingBubble = 165 AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) 166 scheduler.post(showAnimation) 167 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 168 } 169 170 /** 171 * Returns a [Runnable] that starts the animation that morphs the handle to the bubble bar. 172 * 173 * Visually, the animation is divided into 2 parts. The stash handle starts animating up and 174 * fading out and then the bubble bar starts animating up and fading in. 175 * 176 * To make the transition from the handle to the bar smooth, the positions and movement of the 2 177 * views must be synchronized. To do that we use a single spring path along the Y axis, starting 178 * from the handle's position to the eventual bar's position. The path is split into 3 parts. 179 * 1. In the first part, we only animate the handle. 180 * 2. In the second part the handle is fully hidden, and the bubble bar is animating in. 181 * 3. The third part is the overshoot of the spring animation, where we make the bubble fully 182 * visible which helps avoiding further updates when we re-enter the second part. 183 */ 184 private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable { 185 moveToState(AnimatingBubble.State.ANIMATING_IN) 186 // prepare the bubble bar for the animation if we're starting fresh 187 if (initialVelocity == null) { 188 bubbleBarView.visibility = VISIBLE 189 bubbleBarView.alpha = 0f 190 bubbleBarView.translationY = 0f 191 bubbleBarView.scaleX = 1f 192 bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y 193 bubbleBarView.setBackgroundScaleX(1f) 194 bubbleBarView.setBackgroundScaleY(1f) 195 bubbleBarView.relativePivotY = 0.5f 196 } 197 198 // this is the offset between the center of the bubble bar and the center of the stash 199 // handle. when the handle becomes invisible and we start animating in the bubble bar, 200 // the translation y is offset by this value to make the transition from the handle to the 201 // bar smooth. 202 val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() 203 val stashedHandleTranslationYForAnimation = 204 bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() 205 val stashedHandleTranslationY = 206 bubbleStashController.getHandleTranslationY() ?: return@Runnable 207 val translationTracker = TranslationTracker(stashedHandleTranslationY) 208 209 // this is the total distance that both the stashed handle and the bubble will be traveling 210 // at the end of the animation the bubble bar will be positioned in the same place when it 211 // shows while we're in an app. 212 val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset 213 val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable 214 animator.setDefaultSpringConfig(springConfig) 215 animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f) 216 animator.addUpdateListener { handle, values -> 217 val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener 218 when { 219 ty >= stashedHandleTranslationYForAnimation -> { 220 // we're in the first leg of the animation. only animate the handle. the bubble 221 // bar remains hidden during this part of the animation 222 223 // map the path [0, stashedHandleTranslationY] to [0,1] 224 val fraction = ty / stashedHandleTranslationYForAnimation 225 handle.alpha = 1 - fraction 226 } 227 ty >= totalTranslationY -> { 228 // this is the second leg of the animation. the handle should be completely 229 // hidden and the bubble bar should start animating in. 230 // it's possible that we're re-entering this leg because this is a spring 231 // animation, so only set the alpha and scale for the bubble bar if we didn't 232 // already fully animate in. 233 handle.alpha = 0f 234 bubbleBarView.translationY = ty - offset 235 if (bubbleBarView.alpha != 1f) { 236 // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1] 237 val fraction = 238 (ty - stashedHandleTranslationYForAnimation) / 239 (totalTranslationY - stashedHandleTranslationYForAnimation) 240 bubbleBarView.alpha = fraction 241 bubbleBarView.scaleY = 242 BUBBLE_ANIMATION_INITIAL_SCALE_Y + 243 (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction 244 if (bubbleBarView.alpha > MIN_ALPHA_FOR_TOUCHABLE) { 245 bubbleStashController.updateTaskbarTouchRegion() 246 } 247 } 248 } 249 else -> { 250 // we're past the target animated value, set the alpha and scale for the bubble 251 // bar so that it's fully visible and no longer changing, but keep moving it 252 // along the animation path 253 bubbleBarView.alpha = 1f 254 bubbleBarView.scaleY = 1f 255 bubbleBarView.translationY = ty - offset 256 bubbleStashController.updateTaskbarTouchRegion() 257 } 258 } 259 translationTracker.updateTyAndExpandIfNeeded(ty) 260 } 261 animator.addEndListener { _, _, _, canceled, _, _, _ -> 262 // if the show animation was canceled, also cancel the hide animation. this is typically 263 // canceled in this class, but could potentially be canceled elsewhere. 264 if (canceled || animatingBubble?.expand == true) { 265 cancelHideAnimation() 266 return@addEndListener 267 } 268 setupAndShowFlyout() 269 270 // the bubble bar is now fully settled in. update taskbar touch region so it's touchable 271 bubbleStashController.updateTaskbarTouchRegion() 272 } 273 animator.start() 274 } 275 276 /** 277 * Returns a [Runnable] that starts the animation that hides the bubble bar and morphs it into 278 * the stashed handle. 279 * 280 * Similarly to the show animation, this is visually divided into 2 parts. We first animate the 281 * bubble bar out, and then animate the stash handle in. At the end of the animation we reset 282 * values of the bubble bar. 283 * 284 * This is a spring animation that goes along the same path of the show animation in the 285 * opposite order, and is split into 3 parts: 286 * 1. In the first part the bubble animates out. 287 * 2. In the second part the bubble bar is fully hidden and the handle animates in. 288 * 3. The third part is the overshoot. The handle is made fully visible. 289 */ 290 private fun buildBubbleBarToHandleAnimation() = Runnable { 291 if (animatingBubble == null) return@Runnable 292 moveToState(AnimatingBubble.State.ANIMATING_OUT) 293 val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() 294 val stashedHandleTranslationY = 295 bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() 296 // this is the total distance that both the stashed handle and the bar will be traveling 297 val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset 298 bubbleStashController.setHandleTranslationY(totalTranslationY) 299 val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable 300 animator.setDefaultSpringConfig(springConfig) 301 animator.spring(DynamicAnimation.TRANSLATION_Y, 0f) 302 animator.addUpdateListener { handle, values -> 303 val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener 304 when { 305 ty <= stashedHandleTranslationY -> { 306 // this is the first leg of the animation. only animate the bubble bar. the 307 // handle is hidden during this part 308 bubbleBarView.translationY = ty - offset 309 // map the path [totalTranslationY, stashedHandleTranslationY] to [0, 1] 310 val fraction = 311 (totalTranslationY - ty) / (totalTranslationY - stashedHandleTranslationY) 312 bubbleBarView.alpha = 1 - fraction 313 bubbleBarView.scaleY = 1 - (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction 314 if (bubbleBarView.alpha > MIN_ALPHA_FOR_TOUCHABLE) { 315 bubbleStashController.updateTaskbarTouchRegion() 316 } 317 } 318 ty <= 0 -> { 319 // this is the second part of the animation. make the bubble bar invisible and 320 // start fading in the handle, but don't update the alpha if it's already fully 321 // visible 322 bubbleBarView.alpha = 0f 323 if (handle.alpha != 1f) { 324 // map the path [stashedHandleTranslationY, 0] to [0, 1] 325 val fraction = (stashedHandleTranslationY - ty) / stashedHandleTranslationY 326 handle.alpha = fraction 327 } 328 } 329 else -> { 330 // we reached the target value. set the alpha of the handle to 1 331 handle.alpha = 1f 332 } 333 } 334 } 335 animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ -> 336 // PhysicsAnimator calls the end listeners when the animation is replaced with a new one 337 // if we're not in ANIMATING_OUT state, then this animation never started and we should 338 // return 339 if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener 340 if (interceptedHandleAnimator) { 341 interceptedHandleAnimator = false 342 // post this to give a PhysicsAnimator a chance to clean up its internal listeners. 343 // otherwise this end listener will be called as soon as we create a new spring 344 // animation 345 scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity)) 346 return@addEndListener 347 } 348 clearAnimatingBubble() 349 if (!canceled) bubbleStashController.stashBubbleBarImmediate() 350 bubbleBarView.relativePivotY = 1f 351 bubbleBarView.scaleY = 1f 352 bubbleStashController.updateTaskbarTouchRegion() 353 } 354 355 val bubble = animatingBubble?.bubbleView?.bubble as? BubbleBarBubble 356 val flyout = bubble?.flyoutMessage 357 if (flyout != null) { 358 bubbleBarFlyoutController.collapseFlyout { 359 onFlyoutRemoved() 360 animator.start() 361 } 362 } else { 363 animator.start() 364 } 365 } 366 367 /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */ 368 fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) { 369 val bubbleView = b.view 370 val animator = PhysicsAnimator.getInstance(bubbleView) 371 if (animator.isRunning()) animator.cancel() 372 // the animation of a new bubble is divided into 2 parts. The first part slides in the 373 // bubble bar and shows the flyout. The second part hides the flyout and transforms the 374 // bubble bar to the handle if we're in an app. 375 val showAnimation = buildBubbleBarSpringInAnimation() 376 val hideAnimation = 377 if (isInApp && !isExpanding) { 378 buildBubbleBarToHandleAnimation() 379 } else { 380 Runnable { 381 moveToState(AnimatingBubble.State.ANIMATING_OUT) 382 bubbleBarFlyoutController.collapseFlyout { 383 onFlyoutRemoved() 384 clearAnimatingBubble() 385 } 386 bubbleStashController.showBubbleBarImmediate() 387 bubbleStashController.updateTaskbarTouchRegion() 388 } 389 } 390 animatingBubble = 391 AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) 392 scheduler.post(showAnimation) 393 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 394 } 395 396 private fun buildBubbleBarSpringInAnimation() = Runnable { 397 moveToState(AnimatingBubble.State.ANIMATING_IN) 398 // prepare the bubble bar for the animation 399 bubbleBarView.translationY = bubbleBarView.height.toFloat() 400 bubbleBarView.visibility = VISIBLE 401 onBubbleBarVisible.run() 402 bubbleBarView.alpha = 1f 403 bubbleBarView.scaleX = 1f 404 bubbleBarView.scaleY = 1f 405 bubbleBarView.setBackgroundScaleX(1f) 406 bubbleBarView.setBackgroundScaleY(1f) 407 408 val translationTracker = TranslationTracker(bubbleBarView.translationY) 409 410 val animator = PhysicsAnimator.getInstance(bubbleBarView) 411 animator.setDefaultSpringConfig(springConfig) 412 animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY) 413 animator.addUpdateListener { _, values -> 414 val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener 415 translationTracker.updateTyAndExpandIfNeeded(ty) 416 bubbleStashController.updateTaskbarTouchRegion() 417 } 418 animator.addEndListener { _, _, _, _, _, _, _ -> 419 if (animatingBubble?.expand == true) { 420 cancelHideAnimation() 421 } else { 422 setupAndShowFlyout() 423 } 424 // the bubble bar is now fully settled in. update taskbar touch region so it's touchable 425 bubbleStashController.updateTaskbarTouchRegion() 426 } 427 animator.start() 428 } 429 430 fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) { 431 if (isAnimating) { 432 interruptAndUpdateAnimatingBubble(b.view, isExpanding) 433 return 434 } 435 cancelAnimationIfPending() 436 437 val bubbleView = b.view 438 val animator = PhysicsAnimator.getInstance(bubbleView) 439 if (animator.isRunning()) animator.cancel() 440 // first bounce the bubble bar and show the flyout. Then hide the flyout. 441 val showAnimation = buildBubbleBarBounceAnimation() 442 val hideAnimation = Runnable { 443 moveToState(AnimatingBubble.State.ANIMATING_OUT) 444 bubbleBarFlyoutController.collapseFlyout { 445 onFlyoutRemoved() 446 clearAnimatingBubble() 447 } 448 bubbleStashController.showBubbleBarImmediate() 449 bubbleStashController.updateTaskbarTouchRegion() 450 } 451 animatingBubble = 452 AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) 453 scheduler.post(showAnimation) 454 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 455 } 456 457 /** 458 * The bubble bar animation when it is collapsed is divided into 2 chained animations. The first 459 * animation is a regular accelerate animation that moves the bubble bar upwards. When it ends 460 * the bubble bar moves back to its initial position with a spring animation. 461 */ 462 private fun buildBubbleBarBounceAnimation() = Runnable { 463 moveToState(AnimatingBubble.State.ANIMATING_IN) 464 val ty = bubbleStashController.bubbleBarTranslationY 465 466 val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView) 467 springBackAnimation.setDefaultSpringConfig(springConfig) 468 springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty) 469 springBackAnimation.addEndListener { _, _, _, _, _, _, _ -> 470 if (animatingBubble?.expand == true) { 471 expandBubbleBar() 472 cancelHideAnimation() 473 } else { 474 setupAndShowFlyout() 475 } 476 } 477 478 // animate the bubble bar up and start the spring back down animation when it ends. 479 ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx) 480 .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS) 481 .withEndAction { 482 springBackAnimation.start() 483 if (animatingBubble?.expand == true) expandBubbleBar() 484 } 485 .start() 486 } 487 488 private fun setupAndShowFlyout() { 489 val bubbleView = animatingBubble?.bubbleView 490 val bubble = bubbleView?.bubble as? BubbleBarBubble 491 val flyout = bubble?.flyoutMessage 492 if (flyout != null) { 493 bubbleBarFlyoutController.setUpAndShowFlyout( 494 BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message), 495 onInit = { bubbleView.suppressDotForBubbleUpdate() }, 496 onEnd = { 497 moveToState(AnimatingBubble.State.IN) 498 bubbleStashController.updateTaskbarTouchRegion() 499 }, 500 ) 501 } else { 502 moveToState(AnimatingBubble.State.IN) 503 } 504 } 505 506 private fun cancelFlyout() { 507 animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ true) 508 bubbleBarFlyoutController.cancelFlyout { bubbleStashController.updateTaskbarTouchRegion() } 509 } 510 511 private fun onFlyoutRemoved() { 512 animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ false) 513 bubbleStashController.updateTaskbarTouchRegion() 514 } 515 516 /** Interrupts the animation due to touching the bubble bar or flyout. */ 517 fun interruptForTouch() { 518 animatingBubble?.hideAnimation?.let { scheduler.cancel(it) } 519 PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning() 520 bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() 521 cancelFlyout() 522 resetBubbleBarPropertiesOnInterrupt() 523 clearAnimatingBubble() 524 } 525 526 /** Notifies the animator that the taskbar area was touched during an animation. */ 527 fun onStashStateChangingWhileAnimating() { 528 animatingBubble?.hideAnimation?.let { scheduler.cancel(it) } 529 cancelFlyout() 530 clearAnimatingBubble() 531 bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() 532 resetBubbleBarPropertiesOnInterrupt() 533 bubbleStashController.onNewBubbleAnimationInterrupted( 534 /* isStashed= */ bubbleBarView.alpha == 0f, 535 bubbleBarView.translationY, 536 ) 537 } 538 539 /** Interrupts the animation due to the IME becoming visible. */ 540 fun interruptForIme() { 541 cancelFlyout() 542 val hideAnimation = animatingBubble?.hideAnimation ?: return 543 scheduler.cancel(hideAnimation) 544 animatingBubble = null 545 bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() 546 resetBubbleBarPropertiesOnInterrupt() 547 // stash the bubble bar since the IME is now visible 548 bubbleStashController.onNewBubbleAnimationInterrupted( 549 /* isStashed= */ true, 550 bubbleBarView.translationY, 551 ) 552 } 553 554 fun expandedWhileAnimating() { 555 val animatingBubble = animatingBubble ?: return 556 this.animatingBubble = animatingBubble.copy(expand = true) 557 // if we're fully in and waiting to hide, cancel the hide animation and clean up 558 if (animatingBubble.state == AnimatingBubble.State.IN) { 559 cancelFlyout() 560 expandBubbleBar() 561 cancelHideAnimation() 562 } 563 } 564 565 private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) { 566 val animatingBubble = animatingBubble ?: return 567 when (animatingBubble.state) { 568 AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started 569 AnimatingBubble.State.ANIMATING_IN -> 570 updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding) 571 AnimatingBubble.State.IN -> 572 updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding) 573 AnimatingBubble.State.ANIMATING_OUT -> 574 updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding) 575 } 576 } 577 578 private fun updateAnimationWhileAnimatingIn( 579 animatingBubble: AnimatingBubble, 580 bubbleView: BubbleView, 581 isExpanding: Boolean, 582 ) { 583 this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) 584 if (!bubbleBarFlyoutController.hasFlyout()) { 585 // if the flyout does not yet exist, then we're only animating the bubble bar. 586 // the animating bubble has been updated, so the when the flyout expands it will 587 // show the right message. we only need to update the dot visibility. 588 bubbleView.updateDotVisibility(/* animate= */ !bubbleStashController.isStashed) 589 return 590 } 591 592 val bubble = bubbleView.bubble as? BubbleBarBubble 593 val flyout = bubble?.flyoutMessage 594 if (flyout != null) { 595 // the flyout is currently expanding and we need to update it with new data 596 bubbleView.suppressDotForBubbleUpdate() 597 bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout) 598 } else { 599 // the flyout is expanding but we don't have new flyout data to update it with, 600 // so cancel the expanding flyout. 601 cancelFlyout() 602 } 603 } 604 605 private fun updateAnimationWhileIn( 606 animatingBubble: AnimatingBubble, 607 bubbleView: BubbleView, 608 isExpanding: Boolean, 609 ) { 610 // unsuppress the current bubble because we are about to hide its flyout 611 animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false) 612 this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) 613 614 // we're currently idle, waiting for the hide animation to start. update the flyout 615 // data and reschedule the hide animation to run later to give the user a chance to 616 // see the new flyout. 617 val hideAnimation = animatingBubble.hideAnimation 618 scheduler.cancel(hideAnimation) 619 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 620 621 val bubble = bubbleView.bubble as? BubbleBarBubble 622 val flyout = bubble?.flyoutMessage 623 if (flyout != null) { 624 bubbleView.suppressDotForBubbleUpdate() 625 bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) { 626 bubbleStashController.updateTaskbarTouchRegion() 627 } 628 } else { 629 cancelFlyout() 630 } 631 } 632 633 private fun updateAnimationWhileAnimatingOut( 634 animatingBubble: AnimatingBubble, 635 bubbleView: BubbleView, 636 isExpanding: Boolean, 637 ) { 638 // unsuppress the current bubble because we are about to hide its flyout 639 animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false) 640 this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) 641 642 // the hide animation already started so it can't be canceled, just post it again 643 val hideAnimation = animatingBubble.hideAnimation 644 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 645 646 val bubble = bubbleView.bubble as? BubbleBarBubble 647 val flyout = bubble?.flyoutMessage 648 if (bubbleBarFlyoutController.hasFlyout()) { 649 // the flyout is collapsing. update it with the new flyout 650 if (flyout != null) { 651 moveToState(AnimatingBubble.State.ANIMATING_IN) 652 bubbleView.suppressDotForBubbleUpdate() 653 bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) { 654 moveToState(AnimatingBubble.State.IN) 655 bubbleStashController.updateTaskbarTouchRegion() 656 } 657 } else { 658 cancelFlyout() 659 moveToState(AnimatingBubble.State.IN) 660 } 661 } else { 662 // the flyout is already gone. if we're animating the handle cancel it. the 663 // animation itself can handle morphing back into the bubble bar and restarting 664 // and show the flyout. 665 val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator() 666 if (handleAnimator != null && handleAnimator.isRunning()) { 667 interceptedHandleAnimator = true 668 handleAnimator.cancel() 669 } 670 671 // if we're not animating the handle, then the hide animation simply hides the 672 // flyout, but if the flyout is gone then the animation has ended. 673 } 674 } 675 676 private fun cancelHideAnimation() { 677 val hideAnimation = animatingBubble?.hideAnimation ?: return 678 scheduler.cancel(hideAnimation) 679 clearAnimatingBubble() 680 bubbleBarView.relativePivotY = 1f 681 bubbleStashController.showBubbleBarImmediate() 682 } 683 684 private fun resetBubbleBarPropertiesOnInterrupt() { 685 bubbleBarView.relativePivotY = 1f 686 bubbleBarView.scaleX = 1f 687 bubbleBarView.scaleY = 1f 688 } 689 690 private fun <T> PhysicsAnimator<T>?.cancelIfRunning() { 691 if (this?.isRunning() == true) cancel() 692 } 693 694 private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator { 695 setDuration(duration) 696 return this 697 } 698 699 private fun ObjectAnimator.withEndAction(endAction: () -> Unit): ObjectAnimator { 700 addListener( 701 object : AnimatorListenerAdapter() { 702 override fun onAnimationEnd(animation: Animator) { 703 endAction() 704 } 705 } 706 ) 707 return this 708 } 709 710 private fun moveToState(state: AnimatingBubble.State) { 711 val animatingBubble = this.animatingBubble ?: return 712 this.animatingBubble = animatingBubble.copy(state = state) 713 if (state == AnimatingBubble.State.ANIMATING_IN) { 714 bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary() 715 } 716 } 717 718 private fun clearAnimatingBubble() { 719 animatingBubble = null 720 bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary() 721 } 722 723 private fun expandBubbleBar() { 724 bubbleBarView.isExpanded = true 725 onExpanded.run() 726 } 727 728 /** 729 * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands 730 * as part of the animation, the expansion should start after the bubble bar reaches the peak 731 * position. 732 */ 733 private inner class TranslationTracker(initialTy: Float) { 734 private var previousTy = initialTy 735 private var startedExpanding = false 736 private var reachedPeak = false 737 738 fun updateTyAndExpandIfNeeded(ty: Float) { 739 if (!reachedPeak) { 740 // the bubble bar is positioned at the bottom of the screen and moves up using 741 // negative ty values. the peak is reached the first time we see a value that is 742 // greater than the previous. 743 if (ty > previousTy) { 744 reachedPeak = true 745 } 746 } 747 val expand = animatingBubble?.expand ?: false 748 if (reachedPeak && expand && !startedExpanding) { 749 expandBubbleBar() 750 startedExpanding = true 751 } 752 previousTy = ty 753 } 754 } 755 } 756