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