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