• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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