• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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