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.PointF 24 import android.graphics.PorterDuff 25 import android.graphics.PorterDuffXfermode 26 import android.graphics.drawable.GradientDrawable 27 import android.util.FloatProperty 28 import android.util.Log 29 import android.util.MathUtils 30 import android.util.TimeUtils 31 import android.view.Choreographer 32 import android.view.View 33 import android.view.ViewGroup 34 import android.view.ViewGroupOverlay 35 import android.view.ViewOverlay 36 import android.view.animation.Interpolator 37 import android.window.WindowAnimationState 38 import com.android.app.animation.Interpolators.LINEAR 39 import com.android.internal.annotations.VisibleForTesting 40 import com.android.internal.dynamicanimation.animation.SpringAnimation 41 import com.android.internal.dynamicanimation.animation.SpringForce 42 import com.android.systemui.Flags 43 import com.android.systemui.Flags.moveTransitionAnimationLayer 44 import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary 45 import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived 46 import java.util.concurrent.Executor 47 import kotlin.math.abs 48 import kotlin.math.max 49 import kotlin.math.min 50 import kotlin.math.roundToInt 51 52 private const val TAG = "TransitionAnimator" 53 54 /** A base class to animate a window (activity or dialog) launch to or return from a view . */ 55 class TransitionAnimator( 56 private val mainExecutor: Executor, 57 private val timings: Timings, 58 private val interpolators: Interpolators, 59 60 /** [springTimings] and [springInterpolators] must either both be null or both not null. */ 61 private val springTimings: SpringTimings? = null, 62 private val springInterpolators: Interpolators? = null, 63 private val springParams: SpringParams = DEFAULT_SPRING_PARAMS, 64 ) { 65 companion object { 66 internal const val DEBUG = false 67 private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) 68 69 /** Default parameters for the multi-spring animator. */ 70 private val DEFAULT_SPRING_PARAMS = 71 SpringParams( 72 centerXStiffness = 450f, 73 centerXDampingRatio = 0.965f, 74 centerYStiffness = 400f, 75 centerYDampingRatio = 0.95f, 76 scaleStiffness = 500f, 77 scaleDampingRatio = 0.99f, 78 ) 79 80 /** 81 * Given the [linearProgress] of a transition animation, return the linear progress of the 82 * sub-animation starting [delay] ms after the transition animation and that lasts 83 * [duration]. 84 */ 85 @JvmStatic 86 fun getProgress( 87 timings: Timings, 88 linearProgress: Float, 89 delay: Long, 90 duration: Long, 91 ): Float { 92 return getProgressInternal( 93 timings.totalDuration.toFloat(), 94 linearProgress, 95 delay.toFloat(), 96 duration.toFloat(), 97 ) 98 } 99 100 /** 101 * Similar to [getProgress] above, bug the delay and duration are expressed as percentages 102 * of the animation duration (between 0f and 1f). 103 */ 104 internal fun getProgress(linearProgress: Float, delay: Float, duration: Float): Float { 105 return getProgressInternal(totalDuration = 1f, linearProgress, delay, duration) 106 } 107 108 private fun getProgressInternal( 109 totalDuration: Float, 110 linearProgress: Float, 111 delay: Float, 112 duration: Float, 113 ): Float { 114 return MathUtils.constrain( 115 (linearProgress * totalDuration - delay) / duration, 116 0.0f, 117 1.0f, 118 ) 119 } 120 121 fun assertReturnAnimations() { 122 check(returnAnimationsEnabled()) { 123 "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag " + 124 "is disabled" 125 } 126 } 127 128 fun returnAnimationsEnabled() = returnAnimationFrameworkLibrary() 129 130 fun assertLongLivedReturnAnimations() { 131 check(longLivedReturnAnimationsEnabled()) { 132 "Long-lived registrations cannot be used when the " + 133 "returnAnimationFrameworkLibrary or the " + 134 "returnAnimationFrameworkLongLived flag are disabled" 135 } 136 } 137 138 fun longLivedReturnAnimationsEnabled() = 139 returnAnimationFrameworkLibrary() && returnAnimationFrameworkLongLived() 140 141 internal fun WindowAnimationState.toTransitionState() = 142 State().also { 143 bounds?.let { b -> 144 it.top = b.top.roundToInt() 145 it.left = b.left.roundToInt() 146 it.bottom = b.bottom.roundToInt() 147 it.right = b.right.roundToInt() 148 } 149 it.bottomCornerRadius = (bottomLeftRadius + bottomRightRadius) / 2 150 it.topCornerRadius = (topLeftRadius + topRightRadius) / 2 151 } 152 153 /** Builds a [FloatProperty] for updating the defined [property] using a spring. */ 154 private fun buildProperty( 155 property: SpringProperty, 156 updateProgress: (SpringState) -> Unit, 157 ): FloatProperty<SpringState> { 158 return object : FloatProperty<SpringState>(property.name) { 159 override fun get(state: SpringState): Float { 160 return property.get(state) 161 } 162 163 override fun setValue(state: SpringState, value: Float) { 164 property.setValue(state, value) 165 updateProgress(state) 166 } 167 } 168 } 169 } 170 171 private val transitionContainerLocation = IntArray(2) 172 private val cornerRadii = FloatArray(8) 173 174 init { 175 check((springTimings == null) == (springInterpolators == null)) 176 } 177 178 /** 179 * A controller that takes care of applying the animation to an expanding view. 180 * 181 * Note that all callbacks (onXXX methods) are all called on the main thread. 182 */ 183 interface Controller { 184 /** 185 * The container in which the view that started the animation will be animating together 186 * with the opening or closing window. 187 * 188 * This will be used to: 189 * - Get the associated [Context]. 190 * - Compute whether we are expanding to or contracting from fully above the transition 191 * container. 192 * - Get the overlay into which we put the window background layer, while the animating 193 * window is not visible (see [openingWindowSyncView]). 194 * 195 * This container can be changed to force this [Controller] to animate the expanding view 196 * inside a different location, for instance to ensure correct layering during the 197 * animation. 198 */ 199 var transitionContainer: ViewGroup 200 201 /** Whether the animation being controlled is a launch or a return. */ 202 val isLaunching: Boolean 203 204 /** 205 * If [isLaunching], the [View] with which the opening app window should be synchronized 206 * once it starts to be visible. Otherwise, the [View] with which the closing app window 207 * should be synchronized until it stops being visible. 208 * 209 * We will also move the window background layer to this view's overlay once the opening 210 * window is visible (if [isLaunching]), or from this view's overlay once the closing window 211 * stop being visible (if ![isLaunching]). 212 * 213 * If null, this will default to [transitionContainer]. 214 */ 215 val openingWindowSyncView: View? 216 get() = null 217 218 /** 219 * Window state for the animation. If [isLaunching], it would correspond to the end state 220 * otherwise the start state. 221 * 222 * If null, the state is inferred from the window targets 223 */ 224 val windowAnimatorState: WindowAnimationState? 225 get() = null 226 227 /** 228 * Return the [State] of the view that will be animated. We will animate from this state to 229 * the final window state. 230 * 231 * Note: This state will be mutated and passed to [onTransitionAnimationProgress] during the 232 * animation. 233 */ 234 fun createAnimatorState(): State 235 236 /** 237 * The animation started. This is typically used to initialize any additional resource 238 * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding 239 * fully above the [transitionContainer]. 240 */ 241 fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {} 242 243 /** The animation made progress and the expandable view [state] should be updated. */ 244 fun onTransitionAnimationProgress(state: State, progress: Float, linearProgress: Float) {} 245 246 /** 247 * The animation ended. This will be called *if and only if* [onTransitionAnimationStart] 248 * was called previously. This is typically used to clean up the resources initialized when 249 * the animation was started. 250 */ 251 fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {} 252 } 253 254 /** The state of an expandable view during a [TransitionAnimator] animation. */ 255 open class State( 256 /** The position of the view in screen space coordinates. */ 257 var top: Int = 0, 258 var bottom: Int = 0, 259 var left: Int = 0, 260 var right: Int = 0, 261 var topCornerRadius: Float = 0f, 262 var bottomCornerRadius: Float = 0f, 263 ) { 264 private val startTop = top 265 266 val width: Int 267 get() = right - left 268 269 val height: Int 270 get() = bottom - top 271 272 open val topChange: Int 273 get() = top - startTop 274 275 val centerX: Float 276 get() = left + width / 2f 277 278 val centerY: Float 279 get() = top + height / 2f 280 281 /** Whether the expanding view should be visible or hidden. */ 282 var visible: Boolean = true 283 } 284 285 /** Encapsulated the state of a multi-spring animation. */ 286 internal class SpringState( 287 // Animated values. 288 var centerX: Float, 289 var centerY: Float, 290 var scale: Float = 0f, 291 292 // Update flags (used to decide whether it's time to update the transition state). 293 var isCenterXUpdated: Boolean = false, 294 var isCenterYUpdated: Boolean = false, 295 var isScaleUpdated: Boolean = false, 296 297 // Completion flags. 298 var isCenterXDone: Boolean = false, 299 var isCenterYDone: Boolean = false, 300 var isScaleDone: Boolean = false, 301 ) { 302 /** Whether all springs composing the animation have settled in the final position. */ 303 val isDone 304 get() = isCenterXDone && isCenterYDone && isScaleDone 305 } 306 307 /** Supported [SpringState] properties with getters and setters to update them. */ 308 private enum class SpringProperty { 309 CENTER_X { 310 override fun get(state: SpringState): Float { 311 return state.centerX 312 } 313 314 override fun setValue(state: SpringState, value: Float) { 315 state.centerX = value 316 state.isCenterXUpdated = true 317 } 318 }, 319 CENTER_Y { 320 override fun get(state: SpringState): Float { 321 return state.centerY 322 } 323 324 override fun setValue(state: SpringState, value: Float) { 325 state.centerY = value 326 state.isCenterYUpdated = true 327 } 328 }, 329 SCALE { 330 override fun get(state: SpringState): Float { 331 return state.scale 332 } 333 334 override fun setValue(state: SpringState, value: Float) { 335 state.scale = value 336 state.isScaleUpdated = true 337 } 338 }; 339 340 /** Extracts the current value of the underlying property from [state]. */ 341 abstract fun get(state: SpringState): Float 342 343 /** Update's the [value] of the underlying property inside [state]. */ 344 abstract fun setValue(state: SpringState, value: Float) 345 } 346 347 interface Animation { 348 /** Start the animation. */ 349 fun start() 350 351 /** Cancel the animation. */ 352 fun cancel() 353 } 354 355 @VisibleForTesting 356 class InterpolatedAnimation(@get:VisibleForTesting val animator: Animator) : Animation { 357 override fun start() { 358 animator.start() 359 } 360 361 override fun cancel() { 362 animator.cancel() 363 } 364 } 365 366 @VisibleForTesting 367 class MultiSpringAnimation 368 internal constructor( 369 @get:VisibleForTesting val springX: SpringAnimation, 370 @get:VisibleForTesting val springY: SpringAnimation, 371 @get:VisibleForTesting val springScale: SpringAnimation, 372 private val springState: SpringState, 373 private val startFrameTime: Long, 374 private val onAnimationStart: Runnable, 375 ) : Animation { 376 @get:VisibleForTesting 377 val isDone 378 get() = springState.isDone 379 380 override fun start() { 381 onAnimationStart.run() 382 383 // If no start frame time is provided, we start the springs normally. 384 if (startFrameTime < 0) { 385 startSprings() 386 return 387 } 388 389 // This function is not guaranteed to be called inside a frame. We try to access the 390 // frame time immediately, but if we're not inside a frame this will throw an exception. 391 // We must then post a callback to be run at the beginning of the next frame. 392 try { 393 initAndStartSprings(Choreographer.getInstance().frameTime) 394 } catch (_: IllegalStateException) { 395 Choreographer.getInstance().postFrameCallback { frameTimeNanos -> 396 initAndStartSprings(frameTimeNanos / TimeUtils.NANOS_PER_MS) 397 } 398 } 399 } 400 401 private fun initAndStartSprings(frameTime: Long) { 402 // Initialize the spring as if it had started at the time that its start state 403 // was created. 404 springX.doAnimationFrame(startFrameTime) 405 springY.doAnimationFrame(startFrameTime) 406 springScale.doAnimationFrame(startFrameTime) 407 // Move the spring time forward to the current frame, so it updates its internal state 408 // following the initial momentum over the elapsed time. 409 springX.doAnimationFrame(frameTime) 410 springY.doAnimationFrame(frameTime) 411 springScale.doAnimationFrame(frameTime) 412 // Actually start the spring. We do this after the previous calls because the framework 413 // doesn't like it when you call doAnimationFrame() after start() with an earlier time. 414 startSprings() 415 } 416 417 private fun startSprings() { 418 springX.start() 419 springY.start() 420 springScale.start() 421 } 422 423 override fun cancel() { 424 springX.cancel() 425 springY.cancel() 426 springScale.cancel() 427 } 428 } 429 430 /** The timings (durations and delays) used by this animator. */ 431 data class Timings( 432 /** The total duration of the animation. */ 433 val totalDuration: Long, 434 435 /** The time to wait before fading out the expanding content. */ 436 val contentBeforeFadeOutDelay: Long, 437 438 /** The duration of the expanding content fade out. */ 439 val contentBeforeFadeOutDuration: Long, 440 441 /** 442 * The time to wait before fading in the expanded content (usually an activity or dialog 443 * window). 444 */ 445 val contentAfterFadeInDelay: Long, 446 447 /** The duration of the expanded content fade in. */ 448 val contentAfterFadeInDuration: Long, 449 ) 450 451 /** 452 * The timings (durations and delays) used by the multi-spring animator. These are expressed as 453 * fractions of 1, similar to how the progress of an animator can be expressed as a float value 454 * between 0 and 1. 455 */ 456 class SpringTimings( 457 /** The portion of animation to wait before fading out the expanding content. */ 458 val contentBeforeFadeOutDelay: Float, 459 460 /** The portion of animation during which the expanding content fades out. */ 461 val contentBeforeFadeOutDuration: Float, 462 463 /** The portion of animation to wait before fading in the expanded content. */ 464 val contentAfterFadeInDelay: Float, 465 466 /** The portion of animation during which the expanded content fades in. */ 467 val contentAfterFadeInDuration: Float, 468 ) 469 470 /** The interpolators used by this animator. */ 471 data class Interpolators( 472 /** The interpolator used for the Y position, width, height and corner radius. */ 473 val positionInterpolator: Interpolator, 474 475 /** 476 * The interpolator used for the X position. This can be different than 477 * [positionInterpolator] to create an arc-path during the animation. 478 */ 479 val positionXInterpolator: Interpolator = positionInterpolator, 480 481 /** The interpolator used when fading out the expanding content. */ 482 val contentBeforeFadeOutInterpolator: Interpolator, 483 484 /** The interpolator used when fading in the expanded content. */ 485 val contentAfterFadeInInterpolator: Interpolator, 486 ) 487 488 /** The parameters (stiffnesses and damping ratios) used by the multi-spring animator. */ 489 data class SpringParams( 490 // Parameters for the X position spring. 491 val centerXStiffness: Float, 492 val centerXDampingRatio: Float, 493 494 // Parameters for the Y position spring. 495 val centerYStiffness: Float, 496 val centerYDampingRatio: Float, 497 498 // Parameters for the scale spring. 499 val scaleStiffness: Float, 500 val scaleDampingRatio: Float, 501 ) 502 503 /** 504 * Start a transition animation controlled by [controller] towards [endState]. An intermediary 505 * layer with [windowBackgroundColor] will fade in then (optionally) fade out above the 506 * expanding view, and should be the same background color as the opening (or closing) window. 507 * 508 * If [fadeWindowBackgroundLayer] is true, then this intermediary layer will fade out during the 509 * second half of the animation (if [Controller.isLaunching] or fade in during the first half of 510 * the animation (if ![Controller.isLaunching]), and will have SRC blending mode (ultimately 511 * punching a hole in the [transition container][Controller.transitionContainer]) iff [drawHole] 512 * is true. 513 * 514 * TODO(b/397646693): remove drawHole altogether. 515 * 516 * If [startVelocity] (expressed in pixels per second) is not null, a multi-spring animation 517 * using it for the initial momentum will be used instead of the default interpolators. In this 518 * case, [startFrameTime] (if non-negative) represents the frame time at which the springs 519 * should be started. 520 */ 521 fun startAnimation( 522 controller: Controller, 523 endState: State, 524 windowBackgroundColor: Int, 525 fadeWindowBackgroundLayer: Boolean = true, 526 drawHole: Boolean = false, 527 startVelocity: PointF? = null, 528 startFrameTime: Long = -1, 529 ): Animation { 530 if (!controller.isLaunching) assertReturnAnimations() 531 if (startVelocity != null) assertLongLivedReturnAnimations() 532 533 // We add an extra layer with the same color as the dialog/app splash screen background 534 // color, which is usually the same color of the app background. We first fade in this layer 535 // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the 536 // transition container and reveal the opening window. 537 val windowBackgroundLayer = 538 GradientDrawable().apply { 539 setColor(windowBackgroundColor) 540 alpha = 0 541 } 542 543 return createAnimation( 544 controller, 545 controller.createAnimatorState(), 546 endState, 547 windowBackgroundLayer, 548 fadeWindowBackgroundLayer, 549 drawHole, 550 startVelocity, 551 startFrameTime, 552 ) 553 .apply { start() } 554 } 555 556 @VisibleForTesting 557 fun createAnimation( 558 controller: Controller, 559 startState: State, 560 endState: State, 561 windowBackgroundLayer: GradientDrawable, 562 fadeWindowBackgroundLayer: Boolean = true, 563 drawHole: Boolean = false, 564 startVelocity: PointF? = null, 565 startFrameTime: Long = -1, 566 ): Animation { 567 val transitionContainer = controller.transitionContainer 568 val transitionContainerOverlay = transitionContainer.overlay 569 val openingWindowSyncView = controller.openingWindowSyncView 570 val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay 571 572 // Whether we should move the [windowBackgroundLayer] into the overlay of 573 // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or 574 // from it once the closing app window stops being visible. 575 // This is necessary as a one-off sync so we can avoid syncing at every frame, especially 576 // in complex interactions like launching an activity from a dialog. See 577 // b/214961273#comment2 for more details. 578 val moveBackgroundLayerWhenAppVisibilityChanges = 579 openingWindowSyncView != null && 580 openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl 581 582 return if (startVelocity != null && springTimings != null && springInterpolators != null) { 583 createSpringAnimation( 584 controller, 585 startState, 586 endState, 587 startVelocity, 588 startFrameTime, 589 windowBackgroundLayer, 590 transitionContainer, 591 transitionContainerOverlay, 592 openingWindowSyncView, 593 openingWindowSyncViewOverlay, 594 fadeWindowBackgroundLayer, 595 drawHole, 596 moveBackgroundLayerWhenAppVisibilityChanges, 597 ) 598 } else { 599 createInterpolatedAnimation( 600 controller, 601 startState, 602 endState, 603 windowBackgroundLayer, 604 transitionContainer, 605 transitionContainerOverlay, 606 openingWindowSyncView, 607 openingWindowSyncViewOverlay, 608 fadeWindowBackgroundLayer, 609 drawHole, 610 moveBackgroundLayerWhenAppVisibilityChanges, 611 ) 612 } 613 } 614 615 /** 616 * Creates an interpolator-based animator that uses [timings] and [interpolators] to calculate 617 * the new bounds and corner radiuses at each frame. 618 */ 619 private fun createInterpolatedAnimation( 620 controller: Controller, 621 state: State, 622 endState: State, 623 windowBackgroundLayer: GradientDrawable, 624 transitionContainer: View, 625 transitionContainerOverlay: ViewGroupOverlay, 626 openingWindowSyncView: View? = null, 627 openingWindowSyncViewOverlay: ViewOverlay? = null, 628 fadeWindowBackgroundLayer: Boolean = true, 629 drawHole: Boolean = false, 630 moveBackgroundLayerWhenAppVisibilityChanges: Boolean = false, 631 ): Animation { 632 // Start state. 633 val startTop = state.top 634 val startBottom = state.bottom 635 val startLeft = state.left 636 val startRight = state.right 637 val startCenterX = (startLeft + startRight) / 2f 638 val startWidth = startRight - startLeft 639 val startTopCornerRadius = state.topCornerRadius 640 val startBottomCornerRadius = state.bottomCornerRadius 641 642 // End state. 643 var endTop = endState.top 644 var endBottom = endState.bottom 645 var endLeft = endState.left 646 var endRight = endState.right 647 var endCenterX = (endLeft + endRight) / 2f 648 var endWidth = endRight - endLeft 649 val endTopCornerRadius = endState.topCornerRadius 650 val endBottomCornerRadius = endState.bottomCornerRadius 651 652 fun maybeUpdateEndState() { 653 if ( 654 endTop != endState.top || 655 endBottom != endState.bottom || 656 endLeft != endState.left || 657 endRight != endState.right 658 ) { 659 endTop = endState.top 660 endBottom = endState.bottom 661 endLeft = endState.left 662 endRight = endState.right 663 endCenterX = (endLeft + endRight) / 2f 664 endWidth = endRight - endLeft 665 } 666 } 667 668 val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState) 669 var movedBackgroundLayer = false 670 671 // Update state. 672 val animator = ValueAnimator.ofFloat(0f, 1f) 673 animator.duration = timings.totalDuration 674 animator.interpolator = LINEAR 675 676 animator.addListener( 677 object : AnimatorListenerAdapter() { 678 override fun onAnimationStart(animation: Animator, isReverse: Boolean) { 679 onAnimationStart( 680 controller, 681 isExpandingFullyAbove, 682 windowBackgroundLayer, 683 transitionContainerOverlay, 684 openingWindowSyncViewOverlay, 685 ) 686 } 687 688 override fun onAnimationEnd(animation: Animator) { 689 onAnimationEnd( 690 controller, 691 isExpandingFullyAbove, 692 windowBackgroundLayer, 693 transitionContainerOverlay, 694 openingWindowSyncViewOverlay, 695 moveBackgroundLayerWhenAppVisibilityChanges, 696 ) 697 } 698 } 699 ) 700 701 animator.addUpdateListener { animation -> 702 maybeUpdateEndState() 703 704 // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non 705 // reversed animation. 706 val linearProgress = animation.animatedFraction 707 val progress = interpolators.positionInterpolator.getInterpolation(linearProgress) 708 val xProgress = interpolators.positionXInterpolator.getInterpolation(linearProgress) 709 710 val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress) 711 val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f 712 713 state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt() 714 state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt() 715 state.left = (xCenter - halfWidth).roundToInt() 716 state.right = (xCenter + halfWidth).roundToInt() 717 718 state.topCornerRadius = 719 MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress) 720 state.bottomCornerRadius = 721 MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress) 722 723 state.visible = checkVisibility(timings, linearProgress, controller.isLaunching) 724 725 if (!movedBackgroundLayer) { 726 movedBackgroundLayer = 727 maybeMoveBackgroundLayer( 728 controller, 729 state, 730 windowBackgroundLayer, 731 transitionContainer, 732 transitionContainerOverlay, 733 openingWindowSyncView, 734 openingWindowSyncViewOverlay, 735 moveBackgroundLayerWhenAppVisibilityChanges, 736 ) 737 } 738 739 val container = 740 if (movedBackgroundLayer) { 741 openingWindowSyncView!! 742 } else { 743 controller.transitionContainer 744 } 745 applyStateToWindowBackgroundLayer( 746 windowBackgroundLayer, 747 state, 748 linearProgress, 749 container, 750 fadeWindowBackgroundLayer, 751 drawHole, 752 controller.isLaunching, 753 useSpring = false, 754 ) 755 756 controller.onTransitionAnimationProgress(state, progress, linearProgress) 757 } 758 759 return InterpolatedAnimation(animator) 760 } 761 762 /** 763 * Creates a compound animator made up of three springs: one for the center x position, one for 764 * the center-y position, and one for the overall scale. 765 * 766 * This animator uses [springTimings] and [springInterpolators] for opacity, based on the scale 767 * progress. 768 */ 769 private fun createSpringAnimation( 770 controller: Controller, 771 startState: State, 772 endState: State, 773 startVelocity: PointF, 774 startFrameTime: Long, 775 windowBackgroundLayer: GradientDrawable, 776 transitionContainer: View, 777 transitionContainerOverlay: ViewGroupOverlay, 778 openingWindowSyncView: View?, 779 openingWindowSyncViewOverlay: ViewOverlay?, 780 fadeWindowBackgroundLayer: Boolean = true, 781 drawHole: Boolean = false, 782 moveBackgroundLayerWhenAppVisibilityChanges: Boolean = false, 783 ): Animation { 784 var springX: SpringAnimation? = null 785 var springY: SpringAnimation? = null 786 var targetX = endState.centerX 787 var targetY = endState.centerY 788 789 var movedBackgroundLayer = false 790 791 fun maybeUpdateEndState() { 792 if (endState.centerX != targetX && endState.centerY != targetY) { 793 targetX = endState.centerX 794 targetY = endState.centerY 795 796 springX?.animateToFinalPosition(targetX) 797 springY?.animateToFinalPosition(targetY) 798 } 799 } 800 801 fun updateProgress(state: SpringState) { 802 if ( 803 !(state.isCenterXUpdated || state.isCenterXDone) || 804 !(state.isCenterYUpdated || state.isCenterYDone) || 805 !(state.isScaleUpdated || state.isScaleDone) 806 ) { 807 // Because all three springs use the same update method, we only actually update 808 // when all properties have received their new value (which could be unchanged from 809 // the previous one), avoiding two redundant calls per frame. 810 return 811 } 812 813 // Reset the update flags. 814 state.isCenterXUpdated = false 815 state.isCenterYUpdated = false 816 state.isScaleUpdated = false 817 818 // Current scale-based values, that will be used to find the new animation bounds. 819 val width = 820 MathUtils.lerp(startState.width.toFloat(), endState.width.toFloat(), state.scale) 821 val height = 822 MathUtils.lerp(startState.height.toFloat(), endState.height.toFloat(), state.scale) 823 824 val newState = 825 State( 826 left = (state.centerX - width / 2).toInt(), 827 top = (state.centerY - height / 2).toInt(), 828 right = (state.centerX + width / 2).toInt(), 829 bottom = (state.centerY + height / 2).toInt(), 830 topCornerRadius = 831 MathUtils.lerp( 832 startState.topCornerRadius, 833 endState.topCornerRadius, 834 state.scale, 835 ), 836 bottomCornerRadius = 837 MathUtils.lerp( 838 startState.bottomCornerRadius, 839 endState.bottomCornerRadius, 840 state.scale, 841 ), 842 ) 843 .apply { 844 visible = checkVisibility(timings, state.scale, controller.isLaunching) 845 } 846 847 if (!movedBackgroundLayer) { 848 movedBackgroundLayer = 849 maybeMoveBackgroundLayer( 850 controller, 851 newState, 852 windowBackgroundLayer, 853 transitionContainer, 854 transitionContainerOverlay, 855 openingWindowSyncView, 856 openingWindowSyncViewOverlay, 857 moveBackgroundLayerWhenAppVisibilityChanges, 858 ) 859 } 860 861 val container = 862 if (movedBackgroundLayer) { 863 openingWindowSyncView!! 864 } else { 865 controller.transitionContainer 866 } 867 applyStateToWindowBackgroundLayer( 868 windowBackgroundLayer, 869 newState, 870 state.scale, 871 container, 872 fadeWindowBackgroundLayer, 873 drawHole, 874 isLaunching = false, 875 useSpring = true, 876 ) 877 878 controller.onTransitionAnimationProgress(newState, state.scale, state.scale) 879 880 maybeUpdateEndState() 881 } 882 883 val springState = SpringState(centerX = startState.centerX, centerY = startState.centerY) 884 val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState) 885 886 /** End listener for each spring, which only does the end work if all springs are done. */ 887 fun onAnimationEnd() { 888 if (!springState.isDone) return 889 onAnimationEnd( 890 controller, 891 isExpandingFullyAbove, 892 windowBackgroundLayer, 893 transitionContainerOverlay, 894 openingWindowSyncViewOverlay, 895 moveBackgroundLayerWhenAppVisibilityChanges, 896 ) 897 } 898 899 springX = 900 SpringAnimation( 901 springState, 902 buildProperty(SpringProperty.CENTER_X) { state -> updateProgress(state) }, 903 ) 904 .apply { 905 spring = 906 SpringForce(endState.centerX).apply { 907 stiffness = springParams.centerXStiffness 908 dampingRatio = springParams.centerXDampingRatio 909 } 910 911 setStartValue(startState.centerX) 912 setStartVelocity(startVelocity.x) 913 setMinValue(min(startState.centerX, endState.centerX)) 914 setMaxValue(max(startState.centerX, endState.centerX)) 915 916 addEndListener { _, _, _, _ -> 917 springState.isCenterXDone = true 918 onAnimationEnd() 919 } 920 } 921 springY = 922 SpringAnimation( 923 springState, 924 buildProperty(SpringProperty.CENTER_Y) { state -> updateProgress(state) }, 925 ) 926 .apply { 927 spring = 928 SpringForce(endState.centerY).apply { 929 stiffness = springParams.centerYStiffness 930 dampingRatio = springParams.centerYDampingRatio 931 } 932 933 setStartValue(startState.centerY) 934 setStartVelocity(startVelocity.y) 935 setMinValue(min(startState.centerY, endState.centerY)) 936 setMaxValue(max(startState.centerY, endState.centerY)) 937 938 addEndListener { _, _, _, _ -> 939 springState.isCenterYDone = true 940 onAnimationEnd() 941 } 942 } 943 val springScale = 944 SpringAnimation( 945 springState, 946 buildProperty(SpringProperty.SCALE) { state -> updateProgress(state) }, 947 ) 948 .apply { 949 spring = 950 SpringForce(1f).apply { 951 stiffness = springParams.scaleStiffness 952 dampingRatio = springParams.scaleDampingRatio 953 } 954 955 setStartValue(0f) 956 setMaxValue(1f) 957 setMinimumVisibleChange(abs(1f / startState.height)) 958 959 addEndListener { _, _, _, _ -> 960 springState.isScaleDone = true 961 onAnimationEnd() 962 } 963 } 964 965 return MultiSpringAnimation(springX, springY, springScale, springState, startFrameTime) { 966 onAnimationStart( 967 controller, 968 isExpandingFullyAbove, 969 windowBackgroundLayer, 970 transitionContainerOverlay, 971 openingWindowSyncViewOverlay, 972 ) 973 } 974 } 975 976 private fun onAnimationStart( 977 controller: Controller, 978 isExpandingFullyAbove: Boolean, 979 windowBackgroundLayer: GradientDrawable, 980 transitionContainerOverlay: ViewGroupOverlay, 981 openingWindowSyncViewOverlay: ViewOverlay?, 982 ) { 983 if (DEBUG) { 984 Log.d(TAG, "Animation started") 985 } 986 controller.onTransitionAnimationStart(isExpandingFullyAbove) 987 988 // Add the drawable to the transition container overlay. Overlays always draw 989 // drawables after views, so we know that it will be drawn above any view added 990 // by the controller. 991 if (controller.isLaunching || openingWindowSyncViewOverlay == null) { 992 transitionContainerOverlay.add(windowBackgroundLayer) 993 } else { 994 openingWindowSyncViewOverlay.add(windowBackgroundLayer) 995 } 996 } 997 998 private fun onAnimationEnd( 999 controller: Controller, 1000 isExpandingFullyAbove: Boolean, 1001 windowBackgroundLayer: GradientDrawable, 1002 transitionContainerOverlay: ViewGroupOverlay, 1003 openingWindowSyncViewOverlay: ViewOverlay?, 1004 moveBackgroundLayerWhenAppVisibilityChanges: Boolean, 1005 ) { 1006 if (DEBUG) { 1007 Log.d(TAG, "Animation ended") 1008 } 1009 1010 val onEnd = { 1011 controller.onTransitionAnimationEnd(isExpandingFullyAbove) 1012 transitionContainerOverlay.remove(windowBackgroundLayer) 1013 1014 if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) { 1015 openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) 1016 } 1017 } 1018 if (Flags.sceneContainer() || !controller.isLaunching) { 1019 // onAnimationEnd is called at the end of the animation, on a Choreographer animation 1020 // tick. During dialog launches, the following calls will move the animated content from 1021 // the dialog overlay back to its original position, and this change must be reflected 1022 // in the next frame given that we then sync the next frame of both the content and 1023 // dialog ViewRoots. During SysUI activity launches, we will instantly collapse the 1024 // shade at the end of the transition. However, if those are rendered by Compose, whose 1025 // compositions are also scheduled on a Choreographer frame, any state change made 1026 // *right now* won't be reflected in the next frame given that a Choreographer frame 1027 // can't schedule another and have it happen in the same frame. So we post the forwarded 1028 // calls to [Controller.onLaunchAnimationEnd] in the main executor, leaving this 1029 // Choreographer frame, ensuring that any state change applied by 1030 // onTransitionAnimationEnd() will be reflected in the same frame. 1031 mainExecutor.execute { onEnd() } 1032 } else { 1033 onEnd() 1034 } 1035 } 1036 1037 /** Returns whether is the controller's view should be visible with the given [timings]. */ 1038 private fun checkVisibility(timings: Timings, progress: Float, isLaunching: Boolean): Boolean { 1039 return if (isLaunching) { 1040 // The expanding view can/should be hidden once it is completely covered by the opening 1041 // window. 1042 getProgress( 1043 timings, 1044 progress, 1045 timings.contentBeforeFadeOutDelay, 1046 timings.contentBeforeFadeOutDuration, 1047 ) < 1 1048 } else { 1049 // The shrinking view can/should be hidden while it is completely covered by the closing 1050 // window. 1051 getProgress( 1052 timings, 1053 progress, 1054 timings.contentAfterFadeInDelay, 1055 timings.contentAfterFadeInDuration, 1056 ) > 0 1057 } 1058 } 1059 1060 /** 1061 * If necessary, moves the background layer from the view container's overlay to the window sync 1062 * view overlay, or vice versa. 1063 * 1064 * @return true if the background layer vwas moved, false otherwise. 1065 */ 1066 private fun maybeMoveBackgroundLayer( 1067 controller: Controller, 1068 state: State, 1069 windowBackgroundLayer: GradientDrawable, 1070 transitionContainer: View, 1071 transitionContainerOverlay: ViewGroupOverlay, 1072 openingWindowSyncView: View?, 1073 openingWindowSyncViewOverlay: ViewOverlay?, 1074 moveBackgroundLayerWhenAppVisibilityChanges: Boolean, 1075 ): Boolean { 1076 if ( 1077 controller.isLaunching && moveBackgroundLayerWhenAppVisibilityChanges && !state.visible 1078 ) { 1079 // The expanding view is not visible, so the opening app is visible. If this is the 1080 // first frame when it happens, trigger a one-off sync and move the background layer 1081 // in its new container. 1082 transitionContainerOverlay.remove(windowBackgroundLayer) 1083 openingWindowSyncViewOverlay!!.add(windowBackgroundLayer) 1084 1085 ViewRootSync.synchronizeNextDraw( 1086 transitionContainer, 1087 openingWindowSyncView!!, 1088 then = {}, 1089 ) 1090 1091 return true 1092 } else if ( 1093 !controller.isLaunching && moveBackgroundLayerWhenAppVisibilityChanges && state.visible 1094 ) { 1095 // The contracting view is now visible, so the closing app is not. If this is the first 1096 // frame when it happens, trigger a one-off sync and move the background layer in its 1097 // new container. 1098 openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer) 1099 transitionContainerOverlay.add(windowBackgroundLayer) 1100 1101 ViewRootSync.synchronizeNextDraw( 1102 openingWindowSyncView!!, 1103 transitionContainer, 1104 then = {}, 1105 ) 1106 1107 return true 1108 } 1109 1110 return false 1111 } 1112 1113 /** Return whether we are expanding fully above the [transitionContainer]. */ 1114 internal fun isExpandingFullyAbove(transitionContainer: View, endState: State): Boolean { 1115 transitionContainer.getLocationOnScreen(transitionContainerLocation) 1116 return endState.top <= transitionContainerLocation[1] && 1117 endState.bottom >= transitionContainerLocation[1] + transitionContainer.height && 1118 endState.left <= transitionContainerLocation[0] && 1119 endState.right >= transitionContainerLocation[0] + transitionContainer.width 1120 } 1121 1122 private fun applyStateToWindowBackgroundLayer( 1123 drawable: GradientDrawable, 1124 state: State, 1125 linearProgress: Float, 1126 transitionContainer: View, 1127 fadeWindowBackgroundLayer: Boolean, 1128 drawHole: Boolean, 1129 isLaunching: Boolean, 1130 useSpring: Boolean, 1131 ) { 1132 // Update position. 1133 transitionContainer.getLocationOnScreen(transitionContainerLocation) 1134 drawable.setBounds( 1135 state.left - transitionContainerLocation[0], 1136 state.top - transitionContainerLocation[1], 1137 state.right - transitionContainerLocation[0], 1138 state.bottom - transitionContainerLocation[1], 1139 ) 1140 1141 // Update radius. 1142 cornerRadii[0] = state.topCornerRadius 1143 cornerRadii[1] = state.topCornerRadius 1144 cornerRadii[2] = state.topCornerRadius 1145 cornerRadii[3] = state.topCornerRadius 1146 cornerRadii[4] = state.bottomCornerRadius 1147 cornerRadii[5] = state.bottomCornerRadius 1148 cornerRadii[6] = state.bottomCornerRadius 1149 cornerRadii[7] = state.bottomCornerRadius 1150 drawable.cornerRadii = cornerRadii 1151 1152 val interpolators: Interpolators 1153 val fadeInProgress: Float 1154 val fadeOutProgress: Float 1155 if (useSpring) { 1156 interpolators = springInterpolators!! 1157 val timings = springTimings!! 1158 fadeInProgress = 1159 getProgress( 1160 linearProgress, 1161 timings.contentBeforeFadeOutDelay, 1162 timings.contentBeforeFadeOutDuration, 1163 ) 1164 fadeOutProgress = 1165 getProgress( 1166 linearProgress, 1167 timings.contentAfterFadeInDelay, 1168 timings.contentAfterFadeInDuration, 1169 ) 1170 } else { 1171 interpolators = this.interpolators 1172 fadeInProgress = 1173 getProgress( 1174 timings, 1175 linearProgress, 1176 timings.contentBeforeFadeOutDelay, 1177 timings.contentBeforeFadeOutDuration, 1178 ) 1179 fadeOutProgress = 1180 getProgress( 1181 timings, 1182 linearProgress, 1183 timings.contentAfterFadeInDelay, 1184 timings.contentAfterFadeInDuration, 1185 ) 1186 } 1187 1188 // We first fade in the background layer to hide the expanding view, then fade it out with 1189 // SRC mode to draw a hole punch in the status bar and reveal the opening window (if 1190 // needed). If !isLaunching, the reverse happens. 1191 if (isLaunching) { 1192 if (fadeInProgress < 1) { 1193 val alpha = 1194 interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) 1195 drawable.alpha = (alpha * 0xFF).roundToInt() 1196 } else if (fadeWindowBackgroundLayer) { 1197 val alpha = 1198 1 - 1199 interpolators.contentAfterFadeInInterpolator.getInterpolation( 1200 fadeOutProgress 1201 ) 1202 drawable.alpha = (alpha * 0xFF).roundToInt() 1203 1204 if (drawHole) { 1205 drawable.setXfermode(SRC_MODE) 1206 } 1207 } else if (moveTransitionAnimationLayer() && fadeOutProgress >= 1 && drawHole) { 1208 // If [drawHole] is true, draw it once the opening content is done fading in. 1209 drawable.alpha = 0x00 1210 drawable.setXfermode(SRC_MODE) 1211 } else { 1212 drawable.alpha = 0xFF 1213 } 1214 } else { 1215 if (fadeInProgress < 1 && fadeWindowBackgroundLayer) { 1216 val alpha = 1217 interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) 1218 drawable.alpha = (alpha * 0xFF).roundToInt() 1219 1220 if (drawHole) { 1221 drawable.setXfermode(SRC_MODE) 1222 } 1223 } else { 1224 val alpha = 1225 1 - 1226 interpolators.contentAfterFadeInInterpolator.getInterpolation( 1227 fadeOutProgress 1228 ) 1229 drawable.alpha = (alpha * 0xFF).roundToInt() 1230 drawable.setXfermode(null) 1231 } 1232 } 1233 } 1234 } 1235