1 /* <lambda>null2 * Copyright (C) 2021 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.systemui.animation 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.graphics.PorterDuff 24 import android.graphics.PorterDuffXfermode 25 import android.graphics.drawable.GradientDrawable 26 import android.util.Log 27 import android.util.MathUtils 28 import android.view.View 29 import android.view.ViewGroup 30 import android.view.animation.Interpolator 31 import androidx.annotation.VisibleForTesting 32 import com.android.app.animation.Interpolators.LINEAR 33 import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary 34 import java.util.concurrent.Executor 35 import kotlin.math.roundToInt 36 37 private const val TAG = "TransitionAnimator" 38 39 /** A base class to animate a window (activity or dialog) launch to or return from a view . */ 40 class TransitionAnimator( 41 private val mainExecutor: Executor, 42 private val timings: Timings, 43 private val interpolators: Interpolators, 44 ) { 45 companion object { 46 internal const val DEBUG = false 47 private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) 48 49 /** 50 * Given the [linearProgress] of a transition animation, return the linear progress of the 51 * sub-animation starting [delay] ms after the transition animation and that lasts 52 * [duration]. 53 */ 54 @JvmStatic 55 fun getProgress( 56 timings: Timings, 57 linearProgress: Float, 58 delay: Long, 59 duration: Long 60 ): Float { 61 return MathUtils.constrain( 62 (linearProgress * timings.totalDuration - delay) / duration, 63 0.0f, 64 1.0f 65 ) 66 } 67 68 internal fun checkReturnAnimationFrameworkFlag() { 69 check(returnAnimationFrameworkLibrary()) { 70 "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is " + 71 "disabled" 72 } 73 } 74 } 75 76 private val transitionContainerLocation = IntArray(2) 77 private val cornerRadii = FloatArray(8) 78 79 /** 80 * A controller that takes care of applying the animation to an expanding view. 81 * 82 * Note that all callbacks (onXXX methods) are all called on the main thread. 83 */ 84 interface Controller { 85 /** 86 * The container in which the view that started the animation will be animating together 87 * with the opening or closing window. 88 * 89 * This will be used to: 90 * - Get the associated [Context]. 91 * - Compute whether we are expanding to or contracting from fully above the transition 92 * container. 93 * - Get the overlay into which we put the window background layer, while the animating 94 * window is not visible (see [openingWindowSyncView]). 95 * 96 * This container can be changed to force this [Controller] to animate the expanding view 97 * inside a different location, for instance to ensure correct layering during the 98 * animation. 99 */ 100 var transitionContainer: ViewGroup 101 102 /** Whether the animation being controlled is a launch or a return. */ 103 val isLaunching: Boolean 104 105 /** 106 * If [isLaunching], the [View] with which the opening app window should be synchronized 107 * once it starts to be visible. Otherwise, the [View] with which the closing app window 108 * should be synchronized until it stops being visible. 109 * 110 * We will also move the window background layer to this view's overlay once the opening 111 * window is visible (if [isLaunching]), or from this view's overlay once the closing window 112 * stop being visible (if ![isLaunching]). 113 * 114 * If null, this will default to [transitionContainer]. 115 */ 116 val openingWindowSyncView: View? 117 get() = null 118 119 /** 120 * Return the [State] of the view that will be animated. We will animate from this state to 121 * the final window state. 122 * 123 * Note: This state will be mutated and passed to [onTransitionAnimationProgress] during the 124 * animation. 125 */ 126 fun createAnimatorState(): State 127 128 /** 129 * The animation started. This is typically used to initialize any additional resource 130 * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding 131 * fully above the [transitionContainer]. 132 */ 133 fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {} 134 135 /** The animation made progress and the expandable view [state] should be updated. */ 136 fun onTransitionAnimationProgress(state: State, progress: Float, linearProgress: Float) {} 137 138 /** 139 * The animation ended. This will be called *if and only if* [onTransitionAnimationStart] 140 * was called previously. This is typically used to clean up the resources initialized when 141 * the animation was started. 142 */ 143 fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {} 144 } 145 146 /** The state of an expandable view during a [TransitionAnimator] animation. */ 147 open class State( 148 /** The position of the view in screen space coordinates. */ 149 var top: Int = 0, 150 var bottom: Int = 0, 151 var left: Int = 0, 152 var right: Int = 0, 153 var topCornerRadius: Float = 0f, 154 var bottomCornerRadius: Float = 0f 155 ) { 156 private val startTop = top 157 158 val width: Int 159 get() = right - left 160 161 val height: Int 162 get() = bottom - top 163 164 open val topChange: Int 165 get() = top - startTop 166 167 val centerX: Float 168 get() = left + width / 2f 169 170 val centerY: Float 171 get() = top + height / 2f 172 173 /** Whether the expanding view should be visible or hidden. */ 174 var visible: Boolean = true 175 } 176 177 interface Animation { 178 /** Cancel the animation. */ 179 fun cancel() 180 } 181 182 /** The timings (durations and delays) used by this animator. */ 183 data class Timings( 184 /** The total duration of the animation. */ 185 val totalDuration: Long, 186 187 /** The time to wait before fading out the expanding content. */ 188 val contentBeforeFadeOutDelay: Long, 189 190 /** The duration of the expanding content fade out. */ 191 val contentBeforeFadeOutDuration: Long, 192 193 /** 194 * The time to wait before fading in the expanded content (usually an activity or dialog 195 * window). 196 */ 197 val contentAfterFadeInDelay: Long, 198 199 /** The duration of the expanded content fade in. */ 200 val contentAfterFadeInDuration: Long 201 ) 202 203 /** The interpolators used by this animator. */ 204 data class Interpolators( 205 /** The interpolator used for the Y position, width, height and corner radius. */ 206 val positionInterpolator: Interpolator, 207 208 /** 209 * The interpolator used for the X position. This can be different than 210 * [positionInterpolator] to create an arc-path during the animation. 211 */ 212 val positionXInterpolator: Interpolator = positionInterpolator, 213 214 /** The interpolator used when fading out the expanding content. */ 215 val contentBeforeFadeOutInterpolator: Interpolator, 216 217 /** The interpolator used when fading in the expanded content. */ 218 val contentAfterFadeInInterpolator: Interpolator 219 ) 220 221 /** 222 * Start a transition animation controlled by [controller] towards [endState]. An intermediary 223 * layer with [windowBackgroundColor] will fade in then (optionally) fade out above the 224 * expanding view, and should be the same background color as the opening (or closing) window. 225 * 226 * If [fadeWindowBackgroundLayer] is true, then this intermediary layer will fade out during the 227 * second half of the animation (if [Controller.isLaunching] or fade in during the first half of 228 * the animation (if ![Controller.isLaunching]), and will have SRC blending mode (ultimately 229 * punching a hole in the [transition container][Controller.transitionContainer]) iff [drawHole] 230 * is true. 231 */ 232 fun startAnimation( 233 controller: Controller, 234 endState: State, 235 windowBackgroundColor: Int, 236 fadeWindowBackgroundLayer: Boolean = true, 237 drawHole: Boolean = false, 238 ): Animation { 239 if (!controller.isLaunching) checkReturnAnimationFrameworkFlag() 240 241 // We add an extra layer with the same color as the dialog/app splash screen background 242 // color, which is usually the same color of the app background. We first fade in this layer 243 // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the 244 // transition container and reveal the opening window. 245 val windowBackgroundLayer = 246 GradientDrawable().apply { 247 setColor(windowBackgroundColor) 248 alpha = 0 249 } 250 251 val animator = 252 createAnimator( 253 controller, 254 endState, 255 windowBackgroundLayer, 256 fadeWindowBackgroundLayer, 257 drawHole 258 ) 259 animator.start() 260 261 return object : Animation { 262 override fun cancel() { 263 animator.cancel() 264 } 265 } 266 } 267 268 @VisibleForTesting 269 fun createAnimator( 270 controller: Controller, 271 endState: State, 272 windowBackgroundLayer: GradientDrawable, 273 fadeWindowBackgroundLayer: Boolean = true, 274 drawHole: Boolean = false 275 ): ValueAnimator { 276 val state = controller.createAnimatorState() 277 278 // Start state. 279 val startTop = state.top 280 val startBottom = state.bottom 281 val startLeft = state.left 282 val startRight = state.right 283 val startCenterX = (startLeft + startRight) / 2f 284 val startWidth = startRight - startLeft 285 val startTopCornerRadius = state.topCornerRadius 286 val startBottomCornerRadius = state.bottomCornerRadius 287 288 // End state. 289 var endTop = endState.top 290 var endBottom = endState.bottom 291 var endLeft = endState.left 292 var endRight = endState.right 293 var endCenterX = (endLeft + endRight) / 2f 294 var endWidth = endRight - endLeft 295 val endTopCornerRadius = endState.topCornerRadius 296 val endBottomCornerRadius = endState.bottomCornerRadius 297 298 fun maybeUpdateEndState() { 299 if ( 300 endTop != endState.top || 301 endBottom != endState.bottom || 302 endLeft != endState.left || 303 endRight != endState.right 304 ) { 305 endTop = endState.top 306 endBottom = endState.bottom 307 endLeft = endState.left 308 endRight = endState.right 309 endCenterX = (endLeft + endRight) / 2f 310 endWidth = endRight - endLeft 311 } 312 } 313 314 val transitionContainer = controller.transitionContainer 315 val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState) 316 317 // Update state. 318 val animator = ValueAnimator.ofFloat(0f, 1f) 319 animator.duration = timings.totalDuration 320 animator.interpolator = LINEAR 321 322 // Whether we should move the [windowBackgroundLayer] into the overlay of 323 // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or 324 // from it once the closing app window stops being visible. 325 // This is necessary as a one-off sync so we can avoid syncing at every frame, especially 326 // in complex interactions like launching an activity from a dialog. See 327 // b/214961273#comment2 for more details. 328 val openingWindowSyncView = controller.openingWindowSyncView 329 val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay 330 val moveBackgroundLayerWhenAppVisibilityChanges = 331 openingWindowSyncView != null && 332 openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl 333 334 val transitionContainerOverlay = transitionContainer.overlay 335 var movedBackgroundLayer = false 336 337 animator.addListener( 338 object : AnimatorListenerAdapter() { 339 override fun onAnimationStart(animation: Animator, isReverse: Boolean) { 340 if (DEBUG) { 341 Log.d(TAG, "Animation started") 342 } 343 controller.onTransitionAnimationStart(isExpandingFullyAbove) 344 345 // Add the drawable to the transition container overlay. Overlays always draw 346 // drawables after views, so we know that it will be drawn above any view added 347 // by the controller. 348 if (controller.isLaunching || openingWindowSyncViewOverlay == null) { 349 transitionContainerOverlay.add(windowBackgroundLayer) 350 } else { 351 openingWindowSyncViewOverlay.add(windowBackgroundLayer) 352 } 353 } 354 355 override fun onAnimationEnd(animation: Animator) { 356 if (DEBUG) { 357 Log.d(TAG, "Animation ended") 358 } 359 360 // TODO(b/330672236): Post this to the main thread instead so that it does not 361 // flicker with Flexiglass enabled. 362 controller.onTransitionAnimationEnd(isExpandingFullyAbove) 363 transitionContainerOverlay.remove(windowBackgroundLayer) 364 365 if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) { 366 openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) 367 } 368 } 369 } 370 ) 371 372 animator.addUpdateListener { animation -> 373 maybeUpdateEndState() 374 375 // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non 376 // reversed animation. 377 val linearProgress = animation.animatedFraction 378 val progress = interpolators.positionInterpolator.getInterpolation(linearProgress) 379 val xProgress = interpolators.positionXInterpolator.getInterpolation(linearProgress) 380 381 val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress) 382 val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f 383 384 state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt() 385 state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt() 386 state.left = (xCenter - halfWidth).roundToInt() 387 state.right = (xCenter + halfWidth).roundToInt() 388 389 state.topCornerRadius = 390 MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress) 391 state.bottomCornerRadius = 392 MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress) 393 394 state.visible = 395 if (controller.isLaunching) { 396 // The expanding view can/should be hidden once it is completely covered by the 397 // opening window. 398 getProgress( 399 timings, 400 linearProgress, 401 timings.contentBeforeFadeOutDelay, 402 timings.contentBeforeFadeOutDuration 403 ) < 1 404 } else { 405 getProgress( 406 timings, 407 linearProgress, 408 timings.contentAfterFadeInDelay, 409 timings.contentAfterFadeInDuration 410 ) > 0 411 } 412 413 if ( 414 controller.isLaunching && 415 moveBackgroundLayerWhenAppVisibilityChanges && 416 !state.visible && 417 !movedBackgroundLayer 418 ) { 419 // The expanding view is not visible, so the opening app is visible. If this is 420 // the first frame when it happens, trigger a one-off sync and move the 421 // background layer in its new container. 422 movedBackgroundLayer = true 423 424 transitionContainerOverlay.remove(windowBackgroundLayer) 425 openingWindowSyncViewOverlay!!.add(windowBackgroundLayer) 426 427 ViewRootSync.synchronizeNextDraw( 428 transitionContainer, 429 openingWindowSyncView, 430 then = {} 431 ) 432 } else if ( 433 !controller.isLaunching && 434 moveBackgroundLayerWhenAppVisibilityChanges && 435 state.visible && 436 !movedBackgroundLayer 437 ) { 438 // The contracting view is now visible, so the closing app is not. If this is 439 // the first frame when it happens, trigger a one-off sync and move the 440 // background layer in its new container. 441 movedBackgroundLayer = true 442 443 openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer) 444 transitionContainerOverlay.add(windowBackgroundLayer) 445 446 ViewRootSync.synchronizeNextDraw( 447 openingWindowSyncView, 448 transitionContainer, 449 then = {} 450 ) 451 } 452 453 val container = 454 if (movedBackgroundLayer) { 455 openingWindowSyncView!! 456 } else { 457 controller.transitionContainer 458 } 459 460 applyStateToWindowBackgroundLayer( 461 windowBackgroundLayer, 462 state, 463 linearProgress, 464 container, 465 fadeWindowBackgroundLayer, 466 drawHole, 467 controller.isLaunching 468 ) 469 controller.onTransitionAnimationProgress(state, progress, linearProgress) 470 } 471 472 return animator 473 } 474 475 /** Return whether we are expanding fully above the [transitionContainer]. */ 476 internal fun isExpandingFullyAbove(transitionContainer: View, endState: State): Boolean { 477 transitionContainer.getLocationOnScreen(transitionContainerLocation) 478 return endState.top <= transitionContainerLocation[1] && 479 endState.bottom >= transitionContainerLocation[1] + transitionContainer.height && 480 endState.left <= transitionContainerLocation[0] && 481 endState.right >= transitionContainerLocation[0] + transitionContainer.width 482 } 483 484 private fun applyStateToWindowBackgroundLayer( 485 drawable: GradientDrawable, 486 state: State, 487 linearProgress: Float, 488 transitionContainer: View, 489 fadeWindowBackgroundLayer: Boolean, 490 drawHole: Boolean, 491 isLaunching: Boolean 492 ) { 493 // Update position. 494 transitionContainer.getLocationOnScreen(transitionContainerLocation) 495 drawable.setBounds( 496 state.left - transitionContainerLocation[0], 497 state.top - transitionContainerLocation[1], 498 state.right - transitionContainerLocation[0], 499 state.bottom - transitionContainerLocation[1] 500 ) 501 502 // Update radius. 503 cornerRadii[0] = state.topCornerRadius 504 cornerRadii[1] = state.topCornerRadius 505 cornerRadii[2] = state.topCornerRadius 506 cornerRadii[3] = state.topCornerRadius 507 cornerRadii[4] = state.bottomCornerRadius 508 cornerRadii[5] = state.bottomCornerRadius 509 cornerRadii[6] = state.bottomCornerRadius 510 cornerRadii[7] = state.bottomCornerRadius 511 drawable.cornerRadii = cornerRadii 512 513 // We first fade in the background layer to hide the expanding view, then fade it out 514 // with SRC mode to draw a hole punch in the status bar and reveal the opening window. 515 val fadeInProgress = 516 getProgress( 517 timings, 518 linearProgress, 519 timings.contentBeforeFadeOutDelay, 520 timings.contentBeforeFadeOutDuration 521 ) 522 523 if (isLaunching) { 524 if (fadeInProgress < 1) { 525 val alpha = 526 interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) 527 drawable.alpha = (alpha * 0xFF).roundToInt() 528 } else if (fadeWindowBackgroundLayer) { 529 val fadeOutProgress = 530 getProgress( 531 timings, 532 linearProgress, 533 timings.contentAfterFadeInDelay, 534 timings.contentAfterFadeInDuration 535 ) 536 val alpha = 537 1 - 538 interpolators.contentAfterFadeInInterpolator.getInterpolation( 539 fadeOutProgress 540 ) 541 drawable.alpha = (alpha * 0xFF).roundToInt() 542 543 if (drawHole) { 544 drawable.setXfermode(SRC_MODE) 545 } 546 } else { 547 drawable.alpha = 0xFF 548 } 549 } else { 550 if (fadeInProgress < 1 && fadeWindowBackgroundLayer) { 551 val alpha = 552 interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) 553 drawable.alpha = (alpha * 0xFF).roundToInt() 554 555 if (drawHole) { 556 drawable.setXfermode(SRC_MODE) 557 } 558 } else { 559 val fadeOutProgress = 560 getProgress( 561 timings, 562 linearProgress, 563 timings.contentAfterFadeInDelay, 564 timings.contentAfterFadeInDuration 565 ) 566 val alpha = 567 1 - 568 interpolators.contentAfterFadeInInterpolator.getInterpolation( 569 fadeOutProgress 570 ) 571 drawable.alpha = (alpha * 0xFF).roundToInt() 572 drawable.setXfermode(null) 573 } 574 } 575 } 576 } 577