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