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