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.dynamicanimation.animation.DynamicAnimation
22 import androidx.dynamicanimation.animation.SpringForce
23 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
24 import com.android.launcher3.taskbar.bubbles.BubbleBarView
25 import com.android.launcher3.taskbar.bubbles.BubbleStashController
26 import com.android.launcher3.taskbar.bubbles.BubbleView
27 import com.android.wm.shell.shared.animation.PhysicsAnimator
28 
29 /** Handles animations for bubble bar bubbles. */
30 class BubbleBarViewAnimator
31 @JvmOverloads
32 constructor(
33     private val bubbleBarView: BubbleBarView,
34     private val bubbleStashController: BubbleStashController,
35     private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
36 ) {
37 
38     private var animatingBubble: AnimatingBubble? = null
39 
40     private companion object {
41         /** The time to show the flyout. */
42         const val FLYOUT_DELAY_MS: Long = 2500
43         /** The initial scale Y value that the new bubble is set to before the animation starts. */
44         const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f
45         /** The minimum alpha value to make the bubble bar touchable. */
46         const val MIN_ALPHA_FOR_TOUCHABLE = 0.5f
47     }
48 
49     /** Wrapper around the animating bubble with its show and hide animations. */
50     private data class AnimatingBubble(
51         val bubbleView: BubbleView,
52         val showAnimation: Runnable,
53         val hideAnimation: Runnable
54     )
55 
56     /** An interface for scheduling jobs. */
57     interface Scheduler {
58 
59         /** Schedule the given [block] to run. */
60         fun post(block: Runnable)
61 
62         /** Schedule the given [block] to start with a delay of [delayMillis]. */
63         fun postDelayed(delayMillis: Long, block: Runnable)
64 
65         /** Cancel the given [block] if it hasn't started yet. */
66         fun cancel(block: Runnable)
67     }
68 
69     /** A [Scheduler] that uses a Handler to run jobs. */
70     private class HandlerScheduler(private val view: View) : Scheduler {
71 
72         override fun post(block: Runnable) {
73             view.post(block)
74         }
75 
76         override fun postDelayed(delayMillis: Long, block: Runnable) {
77             view.postDelayed(block, delayMillis)
78         }
79 
80         override fun cancel(block: Runnable) {
81             view.removeCallbacks(block)
82         }
83     }
84 
85     private val springConfig =
86         PhysicsAnimator.SpringConfig(
87             stiffness = SpringForce.STIFFNESS_LOW,
88             dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
89         )
90 
91     /** Animates a bubble for the state where the bubble bar is stashed. */
92     fun animateBubbleInForStashed(b: BubbleBarBubble) {
93         val bubbleView = b.view
94         val animator = PhysicsAnimator.getInstance(bubbleView)
95         if (animator.isRunning()) animator.cancel()
96         // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
97         // and the second part hides it after a delay.
98         val showAnimation = buildHandleToBubbleBarAnimation()
99         val hideAnimation = buildBubbleBarToHandleAnimation()
100         animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
101         scheduler.post(showAnimation)
102         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
103     }
104 
105     /**
106      * Returns a [Runnable] that starts the animation that morphs the handle to the bubble bar.
107      *
108      * Visually, the animation is divided into 2 parts. The stash handle starts animating up and
109      * fading out and then the bubble bar starts animating up and fading in.
110      *
111      * To make the transition from the handle to the bar smooth, the positions and movement of the 2
112      * views must be synchronized. To do that we use a single spring path along the Y axis, starting
113      * from the handle's position to the eventual bar's position. The path is split into 3 parts.
114      * 1. In the first part, we only animate the handle.
115      * 2. In the second part the handle is fully hidden, and the bubble bar is animating in.
116      * 3. The third part is the overshoot of the spring animation, where we make the bubble fully
117      *    visible which helps avoiding further updates when we re-enter the second part.
118      */
119     private fun buildHandleToBubbleBarAnimation() = Runnable {
120         // prepare the bubble bar for the animation
121         bubbleBarView.onAnimatingBubbleStarted()
122         bubbleBarView.visibility = VISIBLE
123         bubbleBarView.alpha = 0f
124         bubbleBarView.translationY = 0f
125         bubbleBarView.scaleX = 1f
126         bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
127         bubbleBarView.relativePivotY = 0.5f
128 
129         // this is the offset between the center of the bubble bar and the center of the stash
130         // handle. when the handle becomes invisible and we start animating in the bubble bar,
131         // the translation y is offset by this value to make the transition from the handle to the
132         // bar smooth.
133         val offset = bubbleStashController.diffBetweenHandleAndBarCenters
134         val stashedHandleTranslationY =
135             bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
136 
137         // this is the total distance that both the stashed handle and the bubble will be traveling
138         // at the end of the animation the bubble bar will be positioned in the same place when it
139         // shows while we're in an app.
140         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
141         val animator = bubbleStashController.stashedHandlePhysicsAnimator
142         animator.setDefaultSpringConfig(springConfig)
143         animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
144         animator.addUpdateListener { handle, values ->
145             val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
146             when {
147                 ty >= stashedHandleTranslationY -> {
148                     // we're in the first leg of the animation. only animate the handle. the bubble
149                     // bar remains hidden during this part of the animation
150 
151                     // map the path [0, stashedHandleTranslationY] to [0,1]
152                     val fraction = ty / stashedHandleTranslationY
153                     handle.alpha = 1 - fraction
154                 }
155                 ty >= totalTranslationY -> {
156                     // this is the second leg of the animation. the handle should be completely
157                     // hidden and the bubble bar should start animating in.
158                     // it's possible that we're re-entering this leg because this is a spring
159                     // animation, so only set the alpha and scale for the bubble bar if we didn't
160                     // already fully animate in.
161                     handle.alpha = 0f
162                     bubbleBarView.translationY = ty - offset
163                     if (bubbleBarView.alpha != 1f) {
164                         // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1]
165                         val fraction =
166                             (ty - stashedHandleTranslationY) /
167                                 (totalTranslationY - stashedHandleTranslationY)
168                         bubbleBarView.alpha = fraction
169                         bubbleBarView.scaleY =
170                             BUBBLE_ANIMATION_INITIAL_SCALE_Y +
171                                 (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction
172                         if (bubbleBarView.alpha > MIN_ALPHA_FOR_TOUCHABLE) {
173                             bubbleStashController.updateTaskbarTouchRegion()
174                         }
175                     }
176                 }
177                 else -> {
178                     // we're past the target animated value, set the alpha and scale for the bubble
179                     // bar so that it's fully visible and no longer changing, but keep moving it
180                     // along the animation path
181                     bubbleBarView.alpha = 1f
182                     bubbleBarView.scaleY = 1f
183                     bubbleBarView.translationY = ty - offset
184                     bubbleStashController.updateTaskbarTouchRegion()
185                 }
186             }
187         }
188         animator.addEndListener { _, _, _, canceled, _, _, _ ->
189             // if the show animation was canceled, also cancel the hide animation. this is typically
190             // canceled in this class, but could potentially be canceled elsewhere.
191             if (canceled) {
192                 val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
193                 scheduler.cancel(hideAnimation)
194                 animatingBubble = null
195                 bubbleBarView.onAnimatingBubbleCompleted()
196                 bubbleBarView.relativePivotY = 1f
197                 return@addEndListener
198             }
199             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
200             bubbleStashController.updateTaskbarTouchRegion()
201         }
202         animator.start()
203     }
204 
205     /**
206      * Returns a [Runnable] that starts the animation that hides the bubble bar and morphs it into
207      * the stashed handle.
208      *
209      * Similarly to the show animation, this is visually divided into 2 parts. We first animate the
210      * bubble bar out, and then animate the stash handle in. At the end of the animation we reset
211      * values of the bubble bar.
212      *
213      * This is a spring animation that goes along the same path of the show animation in the
214      * opposite order, and is split into 3 parts:
215      * 1. In the first part the bubble animates out.
216      * 2. In the second part the bubble bar is fully hidden and the handle animates in.
217      * 3. The third part is the overshoot. The handle is made fully visible.
218      */
219     private fun buildBubbleBarToHandleAnimation() = Runnable {
220         if (animatingBubble == null) return@Runnable
221         val offset = bubbleStashController.diffBetweenHandleAndBarCenters
222         val stashedHandleTranslationY =
223             bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
224         // this is the total distance that both the stashed handle and the bar will be traveling
225         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
226         bubbleStashController.setHandleTranslationY(totalTranslationY)
227         val animator = bubbleStashController.stashedHandlePhysicsAnimator
228         animator.setDefaultSpringConfig(springConfig)
229         animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
230         animator.addUpdateListener { handle, values ->
231             val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
232             when {
233                 ty <= stashedHandleTranslationY -> {
234                     // this is the first leg of the animation. only animate the bubble bar. the
235                     // handle is hidden during this part
236                     bubbleBarView.translationY = ty - offset
237                     // map the path [totalTranslationY, stashedHandleTranslationY] to [0, 1]
238                     val fraction =
239                         (totalTranslationY - ty) / (totalTranslationY - stashedHandleTranslationY)
240                     bubbleBarView.alpha = 1 - fraction
241                     bubbleBarView.scaleY = 1 - (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction
242                     if (bubbleBarView.alpha > MIN_ALPHA_FOR_TOUCHABLE) {
243                         bubbleStashController.updateTaskbarTouchRegion()
244                     }
245                 }
246                 ty <= 0 -> {
247                     // this is the second part of the animation. make the bubble bar invisible and
248                     // start fading in the handle, but don't update the alpha if it's already fully
249                     // visible
250                     bubbleBarView.alpha = 0f
251                     if (handle.alpha != 1f) {
252                         // map the path [stashedHandleTranslationY, 0] to [0, 1]
253                         val fraction = (stashedHandleTranslationY - ty) / stashedHandleTranslationY
254                         handle.alpha = fraction
255                     }
256                 }
257                 else -> {
258                     // we reached the target value. set the alpha of the handle to 1
259                     handle.alpha = 1f
260                 }
261             }
262         }
263         animator.addEndListener { _, _, _, canceled, _, _, _ ->
264             animatingBubble = null
265             if (!canceled) bubbleStashController.stashBubbleBarImmediate()
266             bubbleBarView.onAnimatingBubbleCompleted()
267             bubbleBarView.relativePivotY = 1f
268             bubbleStashController.updateTaskbarTouchRegion()
269         }
270         animator.start()
271     }
272 
273     /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
274     fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
275         val bubbleView = b.view
276         val animator = PhysicsAnimator.getInstance(bubbleView)
277         if (animator.isRunning()) animator.cancel()
278         // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
279         // and the second part hides it after a delay if we are in an app.
280         val showAnimation = buildBubbleBarBounceAnimation()
281         val hideAnimation =
282             if (isInApp && !isExpanding) {
283                 buildBubbleBarToHandleAnimation()
284             } else {
285                 // in this case the bubble bar remains visible so not much to do. once we implement
286                 // the flyout we'll update this runnable to hide it.
287                 Runnable {
288                     animatingBubble = null
289                     bubbleStashController.showBubbleBarImmediate()
290                     bubbleBarView.onAnimatingBubbleCompleted()
291                     bubbleStashController.updateTaskbarTouchRegion()
292                 }
293             }
294         animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
295         scheduler.post(showAnimation)
296         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
297     }
298 
299     private fun buildBubbleBarBounceAnimation() = Runnable {
300         // prepare the bubble bar for the animation
301         bubbleBarView.onAnimatingBubbleStarted()
302         bubbleBarView.translationY = bubbleBarView.height.toFloat()
303         bubbleBarView.visibility = VISIBLE
304         bubbleBarView.alpha = 1f
305         bubbleBarView.scaleX = 1f
306         bubbleBarView.scaleY = 1f
307 
308         val animator = PhysicsAnimator.getInstance(bubbleBarView)
309         animator.setDefaultSpringConfig(springConfig)
310         animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
311         animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() }
312         animator.addEndListener { _, _, _, _, _, _, _ ->
313             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
314             bubbleStashController.updateTaskbarTouchRegion()
315         }
316         animator.start()
317     }
318 
319     /** Handles touching the animating bubble bar. */
320     fun onBubbleBarTouchedWhileAnimating() {
321         PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
322         bubbleStashController.stashedHandlePhysicsAnimator.cancelIfRunning()
323         val hideAnimation = animatingBubble?.hideAnimation ?: return
324         scheduler.cancel(hideAnimation)
325         bubbleBarView.onAnimatingBubbleCompleted()
326         bubbleBarView.relativePivotY = 1f
327         animatingBubble = null
328     }
329 
330     /** Notifies the animator that the taskbar area was touched during an animation. */
331     fun onStashStateChangingWhileAnimating() {
332         val hideAnimation = animatingBubble?.hideAnimation ?: return
333         scheduler.cancel(hideAnimation)
334         animatingBubble = null
335         bubbleStashController.stashedHandlePhysicsAnimator.cancel()
336         bubbleBarView.onAnimatingBubbleCompleted()
337         bubbleBarView.relativePivotY = 1f
338         bubbleStashController.onNewBubbleAnimationInterrupted(
339             /* isStashed= */ bubbleBarView.alpha == 0f,
340             bubbleBarView.translationY
341         )
342     }
343 
344     private fun <T> PhysicsAnimator<T>.cancelIfRunning() {
345         if (isRunning()) cancel()
346     }
347 }
348