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 if (animatingBubble == null) return@addUpdateListener 219 when { 220 ty >= stashedHandleTranslationYForAnimation -> { 221 // we're in the first leg of the animation. only animate the handle. the bubble 222 // bar remains hidden during this part of the animation 223 224 // map the path [0, stashedHandleTranslationY] to [0,1] 225 val fraction = ty / stashedHandleTranslationYForAnimation 226 handle.alpha = 1 - fraction 227 } 228 ty >= totalTranslationY -> { 229 // this is the second leg of the animation. the handle should be completely 230 // hidden and the bubble bar should start animating in. 231 // it's possible that we're re-entering this leg because this is a spring 232 // animation, so only set the alpha and scale for the bubble bar if we didn't 233 // already fully animate in. 234 handle.alpha = 0f 235 bubbleBarView.translationY = ty - offset 236 if (bubbleBarView.alpha != 1f) { 237 // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1] 238 val fraction = 239 (ty - stashedHandleTranslationYForAnimation) / 240 (totalTranslationY - stashedHandleTranslationYForAnimation) 241 bubbleBarView.alpha = fraction 242 bubbleBarView.scaleY = 243 BUBBLE_ANIMATION_INITIAL_SCALE_Y + 244 (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction 245 if (bubbleBarView.alpha > MIN_ALPHA_FOR_TOUCHABLE) { 246 bubbleStashController.updateTaskbarTouchRegion() 247 } 248 } 249 } 250 else -> { 251 // we're past the target animated value, set the alpha and scale for the bubble 252 // bar so that it's fully visible and no longer changing, but keep moving it 253 // along the animation path 254 bubbleBarView.alpha = 1f 255 bubbleBarView.scaleY = 1f 256 bubbleBarView.translationY = ty - offset 257 bubbleStashController.updateTaskbarTouchRegion() 258 } 259 } 260 translationTracker.updateTyAndExpandIfNeeded(ty) 261 } 262 animator.addEndListener { _, _, _, canceled, _, _, _ -> 263 // if the show animation was canceled, also cancel the hide animation. this is typically 264 // canceled in this class, but could potentially be canceled elsewhere. 265 if (canceled || animatingBubble?.expand == true) { 266 cancelHideAnimation() 267 return@addEndListener 268 } 269 setupAndShowFlyout() 270 271 // the bubble bar is now fully settled in. update taskbar touch region so it's touchable 272 bubbleStashController.updateTaskbarTouchRegion() 273 } 274 animator.start() 275 } 276 277 /** 278 * Returns a [Runnable] that starts the animation that hides the bubble bar and morphs it into 279 * the stashed handle. 280 * 281 * Similarly to the show animation, this is visually divided into 2 parts. We first animate the 282 * bubble bar out, and then animate the stash handle in. At the end of the animation we reset 283 * values of the bubble bar. 284 * 285 * This is a spring animation that goes along the same path of the show animation in the 286 * opposite order, and is split into 3 parts: 287 * 1. In the first part the bubble animates out. 288 * 2. In the second part the bubble bar is fully hidden and the handle animates in. 289 * 3. The third part is the overshoot. The handle is made fully visible. 290 */ 291 private fun buildBubbleBarToHandleAnimation() = Runnable { 292 if (animatingBubble == null) return@Runnable 293 moveToState(AnimatingBubble.State.ANIMATING_OUT) 294 val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() 295 val stashedHandleTranslationY = 296 bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() 297 // this is the total distance that both the stashed handle and the bar will be traveling 298 val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset 299 bubbleStashController.setHandleTranslationY(totalTranslationY) 300 val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable 301 animator.setDefaultSpringConfig(springConfig) 302 animator.spring(DynamicAnimation.TRANSLATION_Y, 0f) 303 animator.addUpdateListener { handle, values -> 304 val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener 305 when { 306 ty <= stashedHandleTranslationY -> { 307 // this is the first leg of the animation. only animate the bubble bar. the 308 // handle is hidden during this part 309 bubbleBarView.translationY = ty - offset 310 // map the path [totalTranslationY, stashedHandleTranslationY] to [0, 1] 311 val fraction = 312 (totalTranslationY - ty) / (totalTranslationY - stashedHandleTranslationY) 313 bubbleBarView.alpha = 1 - fraction 314 bubbleBarView.scaleY = 1 - (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction 315 if (bubbleBarView.alpha > MIN_ALPHA_FOR_TOUCHABLE) { 316 bubbleStashController.updateTaskbarTouchRegion() 317 } 318 } 319 ty <= 0 -> { 320 // this is the second part of the animation. make the bubble bar invisible and 321 // start fading in the handle, but don't update the alpha if it's already fully 322 // visible 323 bubbleBarView.alpha = 0f 324 if (handle.alpha != 1f) { 325 // map the path [stashedHandleTranslationY, 0] to [0, 1] 326 val fraction = (stashedHandleTranslationY - ty) / stashedHandleTranslationY 327 handle.alpha = fraction 328 } 329 } 330 else -> { 331 // we reached the target value. set the alpha of the handle to 1 332 handle.alpha = 1f 333 } 334 } 335 } 336 animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ -> 337 // PhysicsAnimator calls the end listeners when the animation is replaced with a new one 338 // if we're not in ANIMATING_OUT state, then this animation never started and we should 339 // return 340 if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener 341 if (interceptedHandleAnimator) { 342 interceptedHandleAnimator = false 343 // post this to give a PhysicsAnimator a chance to clean up its internal listeners. 344 // otherwise this end listener will be called as soon as we create a new spring 345 // animation 346 scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity)) 347 return@addEndListener 348 } 349 clearAnimatingBubble() 350 if (!canceled) bubbleStashController.stashBubbleBarImmediate() 351 bubbleBarView.relativePivotY = 1f 352 bubbleBarView.scaleY = 1f 353 bubbleStashController.updateTaskbarTouchRegion() 354 } 355 356 val bubble = animatingBubble?.bubbleView?.bubble as? BubbleBarBubble 357 val flyout = bubble?.flyoutMessage 358 if (flyout != null) { 359 bubbleBarFlyoutController.collapseFlyout { 360 onFlyoutRemoved() 361 animator.start() 362 } 363 } else { 364 animator.start() 365 } 366 } 367 368 /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */ 369 fun animateToInitialState( 370 b: BubbleBarBubble, 371 isInApp: Boolean, 372 isExpanding: Boolean, 373 isDragging: Boolean = false, 374 ) { 375 val bubbleView = b.view 376 val animator = PhysicsAnimator.getInstance(bubbleView) 377 if (animator.isRunning()) animator.cancel() 378 // the animation of a new bubble is divided into 2 parts. The first part slides in the 379 // bubble bar and shows the flyout. The second part hides the flyout and transforms the 380 // bubble bar to the handle if we're in an app. 381 val showAnimation = buildBubbleBarSpringInAnimation() 382 val hideAnimation = 383 if (isInApp && !isExpanding && !isDragging) { 384 buildBubbleBarToHandleAnimation() 385 } else { 386 Runnable { 387 collapseFlyoutAndUpdateState() 388 if (isDragging) return@Runnable 389 bubbleStashController.showBubbleBarImmediate() 390 bubbleStashController.updateTaskbarTouchRegion() 391 } 392 } 393 animatingBubble = 394 AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) 395 scheduler.post(showAnimation) 396 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 397 } 398 399 private fun buildBubbleBarSpringInAnimation() = Runnable { 400 moveToState(AnimatingBubble.State.ANIMATING_IN) 401 // prepare the bubble bar for the animation 402 bubbleBarView.translationY = bubbleBarView.height.toFloat() 403 bubbleBarView.visibility = VISIBLE 404 onBubbleBarVisible.run() 405 bubbleBarView.alpha = 1f 406 bubbleBarView.scaleX = 1f 407 bubbleBarView.scaleY = 1f 408 bubbleBarView.setBackgroundScaleX(1f) 409 bubbleBarView.setBackgroundScaleY(1f) 410 411 val translationTracker = TranslationTracker(bubbleBarView.translationY) 412 413 val animator = PhysicsAnimator.getInstance(bubbleBarView) 414 animator.setDefaultSpringConfig(springConfig) 415 animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY) 416 animator.addUpdateListener { _, values -> 417 val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener 418 translationTracker.updateTyAndExpandIfNeeded(ty) 419 bubbleStashController.updateTaskbarTouchRegion() 420 } 421 animator.addEndListener { _, _, _, _, _, _, _ -> 422 if (animatingBubble?.expand == true) { 423 cancelHideAnimation() 424 } else { 425 setupAndShowFlyout() 426 } 427 // the bubble bar is now fully settled in. update taskbar touch region so it's touchable 428 bubbleStashController.updateTaskbarTouchRegion() 429 } 430 animator.start() 431 } 432 433 fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) { 434 if (isAnimating) { 435 interruptAndUpdateAnimatingBubble(b.view, isExpanding) 436 return 437 } 438 cancelAnimationIfPending() 439 440 val bubbleView = b.view 441 val animator = PhysicsAnimator.getInstance(bubbleView) 442 if (animator.isRunning()) animator.cancel() 443 // first bounce the bubble bar and show the flyout. Then hide the flyout. 444 val showAnimation = buildBubbleBarBounceAnimation() 445 val hideAnimation = Runnable { 446 collapseFlyoutAndUpdateState() 447 bubbleStashController.showBubbleBarImmediate() 448 bubbleStashController.updateTaskbarTouchRegion() 449 } 450 animatingBubble = 451 AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) 452 scheduler.post(showAnimation) 453 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 454 } 455 456 private fun collapseFlyoutAndUpdateState() { 457 moveToState(AnimatingBubble.State.ANIMATING_OUT) 458 bubbleBarFlyoutController.collapseFlyout { 459 onFlyoutRemoved() 460 clearAnimatingBubble() 461 } 462 } 463 464 /** 465 * The bubble bar animation when it is collapsed is divided into 2 chained animations. The first 466 * animation is a regular accelerate animation that moves the bubble bar upwards. When it ends 467 * the bubble bar moves back to its initial position with a spring animation. 468 */ 469 private fun buildBubbleBarBounceAnimation() = Runnable { 470 moveToState(AnimatingBubble.State.ANIMATING_IN) 471 val ty = bubbleStashController.bubbleBarTranslationY 472 473 val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView) 474 springBackAnimation.setDefaultSpringConfig(springConfig) 475 springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty) 476 springBackAnimation.addEndListener { _, _, _, _, _, _, _ -> 477 if (animatingBubble?.expand == true) { 478 expandBubbleBar() 479 cancelHideAnimation() 480 } else { 481 setupAndShowFlyout() 482 } 483 } 484 485 // animate the bubble bar up and start the spring back down animation when it ends. 486 ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx) 487 .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS) 488 .withEndAction { 489 springBackAnimation.start() 490 if (animatingBubble?.expand == true) expandBubbleBar() 491 } 492 .start() 493 } 494 495 private fun setupAndShowFlyout() { 496 val bubbleView = animatingBubble?.bubbleView 497 val bubble = bubbleView?.bubble as? BubbleBarBubble 498 val flyout = bubble?.flyoutMessage 499 if (flyout != null) { 500 bubbleBarFlyoutController.setUpAndShowFlyout( 501 BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message), 502 onInit = { bubbleView.suppressDotForBubbleUpdate() }, 503 onEnd = { 504 moveToState(AnimatingBubble.State.IN) 505 bubbleStashController.updateTaskbarTouchRegion() 506 }, 507 ) 508 } else { 509 moveToState(AnimatingBubble.State.IN) 510 } 511 } 512 513 private fun cancelFlyout() { 514 animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ true) 515 bubbleBarFlyoutController.cancelFlyout { bubbleStashController.updateTaskbarTouchRegion() } 516 } 517 518 private fun onFlyoutRemoved() { 519 animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ false) 520 bubbleStashController.updateTaskbarTouchRegion() 521 } 522 523 /** Interrupts the animation due to touching the bubble bar or flyout. */ 524 fun interruptForTouch() { 525 animatingBubble?.hideAnimation?.let { scheduler.cancel(it) } 526 PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning() 527 bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() 528 cancelFlyout() 529 resetBubbleBarPropertiesOnInterrupt() 530 clearAnimatingBubble() 531 } 532 533 /** Notifies the animator that the taskbar area was touched during an animation. */ 534 fun onStashStateChangingWhileAnimating() { 535 animatingBubble?.hideAnimation?.let { scheduler.cancel(it) } 536 cancelFlyout() 537 clearAnimatingBubble() 538 bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() 539 resetBubbleBarPropertiesOnInterrupt() 540 bubbleStashController.onNewBubbleAnimationInterrupted( 541 /* isStashed= */ bubbleStashController.isStashed, 542 bubbleBarView.translationY, 543 ) 544 } 545 546 /** Interrupts the animation due to the IME becoming visible. */ 547 fun interruptForIme() { 548 cancelFlyout() 549 val hideAnimation = animatingBubble?.hideAnimation ?: return 550 scheduler.cancel(hideAnimation) 551 animatingBubble = null 552 bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() 553 resetBubbleBarPropertiesOnInterrupt() 554 // stash the bubble bar since the IME is now visible 555 bubbleStashController.onNewBubbleAnimationInterrupted( 556 /* isStashed= */ true, 557 bubbleBarView.translationY, 558 ) 559 } 560 561 fun expandedWhileAnimating() { 562 val animatingBubble = animatingBubble ?: return 563 this.animatingBubble = animatingBubble.copy(expand = true) 564 // if we're fully in and waiting to hide, cancel the hide animation and clean up 565 if (animatingBubble.state == AnimatingBubble.State.IN) { 566 cancelFlyout() 567 expandBubbleBar() 568 cancelHideAnimation() 569 } 570 } 571 572 private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) { 573 val animatingBubble = animatingBubble ?: return 574 when (animatingBubble.state) { 575 AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started 576 AnimatingBubble.State.ANIMATING_IN -> 577 updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding) 578 AnimatingBubble.State.IN -> 579 updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding) 580 AnimatingBubble.State.ANIMATING_OUT -> 581 updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding) 582 } 583 } 584 585 private fun updateAnimationWhileAnimatingIn( 586 animatingBubble: AnimatingBubble, 587 bubbleView: BubbleView, 588 isExpanding: Boolean, 589 ) { 590 this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) 591 if (!bubbleBarFlyoutController.hasFlyout()) { 592 // if the flyout does not yet exist, then we're only animating the bubble bar. 593 // the animating bubble has been updated, so the when the flyout expands it will 594 // show the right message. we only need to update the dot visibility. 595 bubbleView.updateDotVisibility(/* animate= */ !bubbleStashController.isStashed) 596 return 597 } 598 599 val bubble = bubbleView.bubble as? BubbleBarBubble 600 val flyout = bubble?.flyoutMessage 601 if (flyout != null) { 602 // the flyout is currently expanding and we need to update it with new data 603 bubbleView.suppressDotForBubbleUpdate() 604 bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout) 605 } else { 606 // the flyout is expanding but we don't have new flyout data to update it with, 607 // so cancel the expanding flyout. 608 cancelFlyout() 609 } 610 } 611 612 private fun updateAnimationWhileIn( 613 animatingBubble: AnimatingBubble, 614 bubbleView: BubbleView, 615 isExpanding: Boolean, 616 ) { 617 // unsuppress the current bubble because we are about to hide its flyout 618 animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false) 619 this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) 620 621 // we're currently idle, waiting for the hide animation to start. update the flyout 622 // data and reschedule the hide animation to run later to give the user a chance to 623 // see the new flyout. 624 val hideAnimation = animatingBubble.hideAnimation 625 scheduler.cancel(hideAnimation) 626 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 627 628 val bubble = bubbleView.bubble as? BubbleBarBubble 629 val flyout = bubble?.flyoutMessage 630 if (flyout != null) { 631 bubbleView.suppressDotForBubbleUpdate() 632 bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) { 633 bubbleStashController.updateTaskbarTouchRegion() 634 } 635 } else { 636 cancelFlyout() 637 } 638 } 639 640 private fun updateAnimationWhileAnimatingOut( 641 animatingBubble: AnimatingBubble, 642 bubbleView: BubbleView, 643 isExpanding: Boolean, 644 ) { 645 // unsuppress the current bubble because we are about to hide its flyout 646 animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false) 647 this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) 648 649 // the hide animation already started so it can't be canceled, just post it again 650 val hideAnimation = animatingBubble.hideAnimation 651 scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) 652 653 val bubble = bubbleView.bubble as? BubbleBarBubble 654 val flyout = bubble?.flyoutMessage 655 if (bubbleBarFlyoutController.hasFlyout()) { 656 // the flyout is collapsing. update it with the new flyout 657 if (flyout != null) { 658 moveToState(AnimatingBubble.State.ANIMATING_IN) 659 bubbleView.suppressDotForBubbleUpdate() 660 bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) { 661 moveToState(AnimatingBubble.State.IN) 662 bubbleStashController.updateTaskbarTouchRegion() 663 } 664 } else { 665 cancelFlyout() 666 moveToState(AnimatingBubble.State.IN) 667 } 668 } else { 669 // the flyout is already gone. if we're animating the handle cancel it. the 670 // animation itself can handle morphing back into the bubble bar and restarting 671 // and show the flyout. 672 val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator() 673 if (handleAnimator != null && handleAnimator.isRunning()) { 674 interceptedHandleAnimator = true 675 handleAnimator.cancel() 676 } 677 678 // if we're not animating the handle, then the hide animation simply hides the 679 // flyout, but if the flyout is gone then the animation has ended. 680 } 681 } 682 683 private fun cancelHideAnimation() { 684 val hideAnimation = animatingBubble?.hideAnimation ?: return 685 scheduler.cancel(hideAnimation) 686 clearAnimatingBubble() 687 bubbleBarView.relativePivotY = 1f 688 bubbleStashController.showBubbleBarImmediate() 689 } 690 691 private fun resetBubbleBarPropertiesOnInterrupt() { 692 bubbleBarView.relativePivotY = 1f 693 bubbleBarView.scaleX = 1f 694 bubbleBarView.scaleY = 1f 695 } 696 697 private fun <T> PhysicsAnimator<T>?.cancelIfRunning() { 698 if (this?.isRunning() == true) cancel() 699 } 700 701 private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator { 702 setDuration(duration) 703 return this 704 } 705 706 private fun ObjectAnimator.withEndAction(endAction: () -> Unit): ObjectAnimator { 707 addListener( 708 object : AnimatorListenerAdapter() { 709 override fun onAnimationEnd(animation: Animator) { 710 endAction() 711 } 712 } 713 ) 714 return this 715 } 716 717 private fun moveToState(state: AnimatingBubble.State) { 718 val animatingBubble = this.animatingBubble ?: return 719 this.animatingBubble = animatingBubble.copy(state = state) 720 if (state == AnimatingBubble.State.ANIMATING_IN) { 721 bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary() 722 } 723 } 724 725 private fun clearAnimatingBubble() { 726 animatingBubble = null 727 bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary() 728 } 729 730 private fun expandBubbleBar() { 731 bubbleBarView.isExpanded = true 732 onExpanded.run() 733 } 734 735 /** 736 * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands 737 * as part of the animation, the expansion should start after the bubble bar reaches the peak 738 * position. 739 */ 740 private inner class TranslationTracker(initialTy: Float) { 741 private var previousTy = initialTy 742 private var startedExpanding = false 743 private var reachedPeak = false 744 745 fun updateTyAndExpandIfNeeded(ty: Float) { 746 if (!reachedPeak) { 747 // the bubble bar is positioned at the bottom of the screen and moves up using 748 // negative ty values. the peak is reached the first time we see a value that is 749 // greater than the previous. 750 if (ty > previousTy) { 751 reachedPeak = true 752 } 753 } 754 val expand = animatingBubble?.expand ?: false 755 if (reachedPeak && expand && !startedExpanding) { 756 expandBubbleBar() 757 startedExpanding = true 758 } 759 previousTy = ty 760 } 761 } 762 } 763