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