1 /*
2  * 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.app.Dialog
23 import android.graphics.Color
24 import android.graphics.Rect
25 import android.os.Looper
26 import android.service.dreams.IDreamManager
27 import android.util.Log
28 import android.util.MathUtils
29 import android.view.GhostView
30 import android.view.SurfaceControl
31 import android.view.View
32 import android.view.ViewGroup
33 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
34 import android.view.ViewRootImpl
35 import android.view.WindowManager
36 import android.widget.FrameLayout
37 import kotlin.math.roundToInt
38 
39 private const val TAG = "DialogLaunchAnimator"
40 
41 /**
42  * A class that allows dialogs to be started in a seamless way from a view that is transforming
43  * nicely into the starting dialog.
44  */
45 class DialogLaunchAnimator @JvmOverloads constructor(
46     private val dreamManager: IDreamManager,
47     private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
48     private var isForTesting: Boolean = false
49 ) {
50     private companion object {
51         private val TIMINGS = ActivityLaunchAnimator.TIMINGS
52 
53         // We use the same interpolator for X and Y axis to make sure the dialog does not move out
54         // of the screen bounds during the animation.
55         private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS.copy(
56             positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
57         )
58 
59         private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
60     }
61 
62     /**
63      * The set of dialogs that were animated using this animator and that are still opened (not
64      * dismissed, but can be hidden).
65      */
66     // TODO(b/201264644): Remove this set.
67     private val openedDialogs = hashSetOf<AnimatedDialog>()
68 
69     /**
70      * Show [dialog] by expanding it from [view]. If [view] is a view inside another dialog that was
71      * shown using this method, then we will animate from that dialog instead.
72      *
73      * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be
74      * animated when the dialog bounds change.
75      *
76      * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
77      * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
78      */
79     @JvmOverloads
80     fun showFromView(
81         dialog: Dialog,
82         view: View,
83         animateBackgroundBoundsChange: Boolean = false
84     ) {
85         if (Looper.myLooper() != Looper.getMainLooper()) {
86             throw IllegalStateException(
87                 "showFromView must be called from the main thread and dialog must be created in " +
88                     "the main thread")
89         }
90 
91         // If the view we are launching from belongs to another dialog, then this means the caller
92         // intent is to launch a dialog from another dialog.
93         val animatedParent = openedDialogs
94             .firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl }
95         val animateFrom = animatedParent?.dialogContentWithBackground ?: view
96 
97         // Make sure we don't run the launch animation from the same view twice at the same time.
98         if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
99             Log.e(TAG, "Not running dialog launch animation as there is already one running")
100             dialog.show()
101             return
102         }
103 
104         animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
105 
106         val animatedDialog = AnimatedDialog(
107                 launchAnimator,
108                 dreamManager,
109                 animateFrom,
110                 onDialogDismissed = { openedDialogs.remove(it) },
111                 dialog = dialog,
112                 animateBackgroundBoundsChange,
113                 animatedParent,
114                 isForTesting
115         )
116 
117         openedDialogs.add(animatedDialog)
118         animatedDialog.start()
119     }
120 
121     /**
122      * Launch [dialog] from [another dialog][animateFrom] that was shown using [showFromView]. This
123      * will allow for dismissing the whole stack.
124      *
125      * @see dismissStack
126      */
127     fun showFromDialog(
128         dialog: Dialog,
129         animateFrom: Dialog,
130         animateBackgroundBoundsChange: Boolean = false
131     ) {
132         val view = openedDialogs
133             .firstOrNull { it.dialog == animateFrom }
134             ?.dialogContentWithBackground
135             ?: throw IllegalStateException(
136                 "The animateFrom dialog was not animated using " +
137                     "DialogLaunchAnimator.showFrom(View|Dialog)")
138         showFromView(dialog, view, animateBackgroundBoundsChange)
139     }
140 
141     /**
142      * Ensure that all dialogs currently shown won't animate into their touch surface when
143      * dismissed.
144      *
145      * This is a temporary API meant to be called right before we both dismiss a dialog and start
146      * an activity, which currently does not look good if we animate the dialog into the touch
147      * surface at the same time as the activity starts.
148      *
149      * TODO(b/193634619): Remove this function and animate dialog into opening activity instead.
150      */
151     fun disableAllCurrentDialogsExitAnimations() {
152         openedDialogs.forEach { it.exitAnimationDisabled = true }
153     }
154 
155     /**
156      * Dismiss [dialog]. If it was launched from another dialog using [showFromView], also dismiss
157      * the stack of dialogs, animating back to the original touchSurface.
158      */
159     fun dismissStack(dialog: Dialog) {
160         openedDialogs
161             .firstOrNull { it.dialog == dialog }
162             ?.let { it.touchSurface = it.prepareForStackDismiss() }
163         dialog.dismiss()
164     }
165 }
166 
167 private class AnimatedDialog(
168     private val launchAnimator: LaunchAnimator,
169     private val dreamManager: IDreamManager,
170 
171     /** The view that triggered the dialog after being tapped. */
172     var touchSurface: View,
173 
174     /**
175      * A callback that will be called with this [AnimatedDialog] after the dialog was
176      * dismissed and the exit animation is done.
177      */
178     private val onDialogDismissed: (AnimatedDialog) -> Unit,
179 
180     /** The dialog to show and animate. */
181     val dialog: Dialog,
182 
183     /** Whether we should animate the dialog background when its bounds change. */
184     animateBackgroundBoundsChange: Boolean,
185 
186     /** Launch animation corresponding to the parent [AnimatedDialog]. */
187     private val parentAnimatedDialog: AnimatedDialog? = null,
188 
189     /**
190      * Whether we are currently running in a test, in which case we need to disable
191      * synchronization.
192      */
193     private val isForTesting: Boolean
194 ) {
195     /**
196      * The DecorView of this dialog window.
197      *
198      * Note that we access this DecorView lazily to avoid accessing it before the dialog is created,
199      * which can sometimes cause crashes (e.g. with the Cast dialog).
200       */
201     private val decorView by lazy { dialog.window!!.decorView as ViewGroup }
202 
203     /**
204      * The dialog content with its background. When animating a fullscreen dialog, this is just the
205      * first ViewGroup of the dialog that has a background. When animating a normal (not fullscreen)
206      * dialog, this is an additional view that serves as a fake window that will have the same size
207      * as the dialog window initially had and to which we will set the dialog window background.
208      */
209     var dialogContentWithBackground: ViewGroup? = null
210 
211     /**
212      * The background color of [dialog], taking into consideration its window background color.
213      */
214     private var originalDialogBackgroundColor = Color.BLACK
215 
216     /**
217      * Whether we are currently launching/showing the dialog by animating it from [touchSurface].
218      */
219     private var isLaunching = true
220 
221     /** Whether we are currently dismissing/hiding the dialog by animating into [touchSurface]. */
222     private var isDismissing = false
223 
224     private var dismissRequested = false
225     var exitAnimationDisabled = false
226 
227     private var isTouchSurfaceGhostDrawn = false
228     private var isOriginalDialogViewLaidOut = false
229 
230     /** A layout listener to animate the dialog height change. */
231     private val backgroundLayoutListener = if (animateBackgroundBoundsChange) {
232         AnimatedBoundsLayoutListener()
233     } else {
234         null
235     }
236 
237     /*
238      * A layout listener in case the dialog (window) size changes (for instance because of a
239      * configuration change) to ensure that the dialog stays full width.
240      */
241     private var decorViewLayoutListener: View.OnLayoutChangeListener? = null
242 
243     fun start() {
244         // Create the dialog so that its onCreate() method is called, which usually sets the dialog
245         // content.
246         dialog.create()
247 
248         val window = dialog.window!!
249         val isWindowFullScreen =
250             window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
251         val dialogContentWithBackground = if (isWindowFullScreen) {
252             // If the dialog window is already fullscreen, then we look for the first ViewGroup that
253             // has a background (and is not the DecorView, which always has a background) and
254             // animate towards that ViewGroup given that this is probably what represents the actual
255             // dialog view.
256             var viewGroupWithBackground: ViewGroup? = null
257             for (i in 0 until decorView.childCount) {
258                 viewGroupWithBackground = findFirstViewGroupWithBackground(decorView.getChildAt(i))
259                 if (viewGroupWithBackground != null) {
260                     break
261                 }
262             }
263 
264             // Animate that view with the background. Throw if we didn't find one, because otherwise
265             // it's not clear what we should animate.
266             viewGroupWithBackground
267                 ?: throw IllegalStateException("Unable to find ViewGroup with background")
268         } else {
269             // We will make the dialog window (and therefore its DecorView) fullscreen to make it
270             // possible to animate outside its bounds.
271             //
272             // Before that, we add a new View as a child of the DecorView with the same size and
273             // gravity as that DecorView, then we add all original children of the DecorView to that
274             // new View. Finally we remove the background of the DecorView and add it to the new
275             // View, then we make the DecorView fullscreen. This new View now acts as a fake (non
276             // fullscreen) window.
277             //
278             // On top of that, we also add a fullscreen transparent background between the DecorView
279             // and the view that we added so that we can dismiss the dialog when this view is
280             // clicked. This is necessary because DecorView overrides onTouchEvent and therefore we
281             // can't set the click listener directly on the (now fullscreen) DecorView.
282             val fullscreenTransparentBackground = FrameLayout(dialog.context)
283             decorView.addView(
284                 fullscreenTransparentBackground,
285                 0 /* index */,
286                 FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
287             )
288 
289             val dialogContentWithBackground = FrameLayout(dialog.context)
290             dialogContentWithBackground.background = decorView.background
291 
292             // Make the window background transparent. Note that setting the window (or DecorView)
293             // background drawable to null leads to issues with background color (not being
294             // transparent) or with insets that are not refreshed. Therefore we need to set it to
295             // something not null, hence we are using android.R.color.transparent here.
296             window.setBackgroundDrawableResource(android.R.color.transparent)
297 
298             // Close the dialog when clicking outside of it.
299             fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
300             dialogContentWithBackground.isClickable = true
301 
302             // Make sure the transparent and dialog backgrounds are not focusable by accessibility
303             // features.
304             fullscreenTransparentBackground.importantForAccessibility =
305                 View.IMPORTANT_FOR_ACCESSIBILITY_NO
306             dialogContentWithBackground.importantForAccessibility =
307                 View.IMPORTANT_FOR_ACCESSIBILITY_NO
308 
309             fullscreenTransparentBackground.addView(
310                 dialogContentWithBackground,
311                 FrameLayout.LayoutParams(
312                     window.attributes.width,
313                     window.attributes.height,
314                     window.attributes.gravity
315                 )
316             )
317 
318             // Move all original children of the DecorView to the new View we just added.
319             for (i in 1 until decorView.childCount) {
320                 val view = decorView.getChildAt(1)
321                 decorView.removeViewAt(1)
322                 dialogContentWithBackground.addView(view)
323             }
324 
325             // Make the window fullscreen and add a layout listener to ensure it stays fullscreen.
326             window.setLayout(MATCH_PARENT, MATCH_PARENT)
327             decorViewLayoutListener = View.OnLayoutChangeListener {
328                 v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
329                 if (window.attributes.width != MATCH_PARENT ||
330                     window.attributes.height != MATCH_PARENT) {
331                     // The dialog size changed, copy its size to dialogContentWithBackground and
332                     // make the dialog window full screen again.
333                     val layoutParams = dialogContentWithBackground.layoutParams
334                     layoutParams.width = window.attributes.width
335                     layoutParams.height = window.attributes.height
336                     dialogContentWithBackground.layoutParams = layoutParams
337                     window.setLayout(MATCH_PARENT, MATCH_PARENT)
338                 }
339             }
340             decorView.addOnLayoutChangeListener(decorViewLayoutListener)
341 
342             dialogContentWithBackground
343         }
344         this.dialogContentWithBackground = dialogContentWithBackground
345 
346         val background = dialogContentWithBackground.background
347         originalDialogBackgroundColor =
348             GhostedViewLaunchAnimatorController.findGradientDrawable(background)
349                 ?.color
350                 ?.defaultColor ?: Color.BLACK
351 
352         // Make the background view invisible until we start the animation. We use the transition
353         // visibility like GhostView does so that we don't mess up with the accessibility tree (see
354         // b/204944038#comment17).
355         dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE)
356 
357         // Make sure the dialog is visible instantly and does not do any window animation.
358         window.attributes.windowAnimations = R.style.Animation_LaunchAnimation
359 
360         // Start the animation once the background view is properly laid out.
361         dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
362             override fun onLayoutChange(
363                 v: View,
364                 left: Int,
365                 top: Int,
366                 right: Int,
367                 bottom: Int,
368                 oldLeft: Int,
369                 oldTop: Int,
370                 oldRight: Int,
371                 oldBottom: Int
372             ) {
373                 dialogContentWithBackground.removeOnLayoutChangeListener(this)
374 
375                 isOriginalDialogViewLaidOut = true
376                 maybeStartLaunchAnimation()
377             }
378         })
379 
380         // Disable the dim. We will enable it once we start the animation.
381         window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
382 
383         // Override the dialog dismiss() so that we can animate the exit before actually dismissing
384         // the dialog.
385         dialog.setDismissOverride(this::onDialogDismissed)
386 
387         // Show the dialog.
388         dialog.show()
389 
390         addTouchSurfaceGhost()
391     }
392 
393     private fun addTouchSurfaceGhost() {
394         if (decorView.viewRootImpl == null) {
395             // Make sure that we have access to the dialog view root to synchronize the creation of
396             // the ghost.
397             decorView.post(::addTouchSurfaceGhost)
398             return
399         }
400 
401         // Create a ghost of the touch surface (which will make the touch surface invisible) and add
402         // it to the host dialog. We trigger a one off synchronization to make sure that this is
403         // done in sync between the two different windows.
404         synchronizeNextDraw(then = {
405             isTouchSurfaceGhostDrawn = true
406             maybeStartLaunchAnimation()
407         })
408         GhostView.addGhost(touchSurface, decorView)
409 
410         // The ghost of the touch surface was just created, so the touch surface is currently
411         // invisible. We need to make sure that it stays invisible as long as the dialog is shown or
412         // animating.
413         (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
414     }
415 
416     /**
417      * Synchronize the next draw of the touch surface and dialog view roots so that they are
418      * performed at the same time, in the same transaction. This is necessary to make sure that the
419      * ghost of the touch surface is drawn at the same time as the touch surface is made invisible
420      * (or inversely, removed from the UI when the touch surface is made visible).
421      */
422     private fun synchronizeNextDraw(then: () -> Unit) {
423         if (isForTesting || !touchSurface.isAttachedToWindow || touchSurface.viewRootImpl == null ||
424             !decorView.isAttachedToWindow || decorView.viewRootImpl == null) {
425             // No need to synchronize if either the touch surface or dialog view is not attached
426             // to a window.
427             then()
428             return
429         }
430 
431         // Consume the next frames of both view roots to make sure the ghost view is drawn at
432         // exactly the same time as when the touch surface is made invisible.
433         var remainingTransactions = 0
434         val mergedTransactions = SurfaceControl.Transaction()
435 
436         fun onTransaction(transaction: SurfaceControl.Transaction?) {
437             remainingTransactions--
438             transaction?.let { mergedTransactions.merge(it) }
439 
440             if (remainingTransactions == 0) {
441                 mergedTransactions.apply()
442                 then()
443             }
444         }
445 
446         fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
447             if (viewRootImpl.consumeNextDraw(::onTransaction)) {
448                 remainingTransactions++
449 
450                 // Make sure we trigger a traversal.
451                 viewRootImpl.view.invalidate()
452             }
453         }
454 
455         consumeNextDraw(touchSurface.viewRootImpl)
456         consumeNextDraw(decorView.viewRootImpl)
457 
458         if (remainingTransactions == 0) {
459             then()
460         }
461     }
462 
463     private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
464         if (view !is ViewGroup) {
465             return null
466         }
467 
468         if (view.background != null) {
469             return view
470         }
471 
472         for (i in 0 until view.childCount) {
473             val match = findFirstViewGroupWithBackground(view.getChildAt(i))
474             if (match != null) {
475                 return match
476             }
477         }
478 
479         return null
480     }
481 
482     private fun maybeStartLaunchAnimation() {
483         if (!isTouchSurfaceGhostDrawn || !isOriginalDialogViewLaidOut) {
484             return
485         }
486 
487         // Show the background dim.
488         dialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
489 
490         startAnimation(
491             isLaunching = true,
492             onLaunchAnimationStart = {
493                 // Remove the temporary ghost. Another ghost (that ghosts only the touch surface
494                 // content, and not its background) will be added right after this and will be
495                 // animated.
496                 GhostView.removeGhost(touchSurface)
497             },
498             onLaunchAnimationEnd = {
499                 touchSurface.setTag(R.id.launch_animation_running, null)
500 
501                 // We hide the touch surface when the dialog is showing. We will make this
502                 // view visible again when dismissing the dialog.
503                 touchSurface.visibility = View.INVISIBLE
504 
505                 isLaunching = false
506 
507                 // dismiss was called during the animation, dismiss again now to actually
508                 // dismiss.
509                 if (dismissRequested) {
510                     dialog.dismiss()
511                 }
512 
513                 // If necessary, we animate the dialog background when its bounds change. We do it
514                 // at the end of the launch animation, because the lauch animation already correctly
515                 // handles bounds changes.
516                 if (backgroundLayoutListener != null) {
517                     dialogContentWithBackground!!
518                         .addOnLayoutChangeListener(backgroundLayoutListener)
519                 }
520             }
521         )
522     }
523 
524     private fun onDialogDismissed() {
525         if (Looper.myLooper() != Looper.getMainLooper()) {
526             dialog.context.mainExecutor.execute { onDialogDismissed() }
527             return
528         }
529 
530         // TODO(b/193634619): Support interrupting the launch animation in the middle.
531         if (isLaunching) {
532             dismissRequested = true
533             return
534         }
535 
536         if (isDismissing) {
537             return
538         }
539 
540         isDismissing = true
541         hideDialogIntoView { animationRan: Boolean ->
542             if (animationRan) {
543                 // Instantly dismiss the dialog if we ran the animation into view. If it was
544                 // skipped, dismiss() will run the window animation (which fades out the dialog).
545                 dialog.hide()
546             }
547 
548             dialog.setDismissOverride(null)
549             dialog.dismiss()
550         }
551     }
552 
553     /**
554      * Hide the dialog into the touch surface and call [onAnimationFinished] when the animation is
555      * done (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually
556      * dismiss the dialog.
557      */
558     private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) {
559         // Remove the layout change listener we have added to the DecorView earlier.
560         if (decorViewLayoutListener != null) {
561             decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
562         }
563 
564         if (!shouldAnimateDialogIntoView()) {
565             Log.i(TAG, "Skipping animation of dialog into the touch surface")
566 
567             // Make sure we allow the touch surface to change its visibility again.
568             (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
569 
570             // If the view is invisible it's probably because of us, so we make it visible again.
571             if (touchSurface.visibility == View.INVISIBLE) {
572                 touchSurface.visibility = View.VISIBLE
573             }
574 
575             onAnimationFinished(false /* instantDismiss */)
576             onDialogDismissed(this@AnimatedDialog)
577             return
578         }
579 
580         startAnimation(
581             isLaunching = false,
582             onLaunchAnimationStart = {
583                 // Remove the dim background as soon as we start the animation.
584                 dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
585             },
586             onLaunchAnimationEnd = {
587                 // Make sure we allow the touch surface to change its visibility again.
588                 (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
589 
590                 touchSurface.visibility = View.VISIBLE
591                 val dialogContentWithBackground = this.dialogContentWithBackground!!
592                 dialogContentWithBackground.visibility = View.INVISIBLE
593 
594                 if (backgroundLayoutListener != null) {
595                     dialogContentWithBackground
596                         .removeOnLayoutChangeListener(backgroundLayoutListener)
597                 }
598 
599                 // Make sure that the removal of the ghost and making the touch surface visible is
600                 // done at the same time.
601                 synchronizeNextDraw(then = {
602                     onAnimationFinished(true /* instantDismiss */)
603                     onDialogDismissed(this@AnimatedDialog)
604                 })
605             }
606         )
607     }
608 
609     private fun startAnimation(
610         isLaunching: Boolean,
611         onLaunchAnimationStart: () -> Unit = {},
612         onLaunchAnimationEnd: () -> Unit = {}
613     ) {
614         // Create 2 ghost controllers to animate both the dialog and the touch surface in the
615         // dialog.
616         val startView = if (isLaunching) touchSurface else dialogContentWithBackground!!
617         val endView = if (isLaunching) dialogContentWithBackground!! else touchSurface
618         val startViewController = GhostedViewLaunchAnimatorController(startView)
619         val endViewController = GhostedViewLaunchAnimatorController(endView)
620         startViewController.launchContainer = decorView
621         endViewController.launchContainer = decorView
622 
623         val endState = endViewController.createAnimatorState()
624         val controller = object : LaunchAnimator.Controller {
625             override var launchContainer: ViewGroup
626                 get() = startViewController.launchContainer
627                 set(value) {
628                     startViewController.launchContainer = value
629                     endViewController.launchContainer = value
630                 }
631 
632             override fun createAnimatorState(): LaunchAnimator.State {
633                 return startViewController.createAnimatorState()
634             }
635 
636             override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
637                 // During launch, onLaunchAnimationStart will be used to remove the temporary touch
638                 // surface ghost so it is important to call this before calling
639                 // onLaunchAnimationStart on the controller (which will create its own ghost).
640                 onLaunchAnimationStart()
641 
642                 startViewController.onLaunchAnimationStart(isExpandingFullyAbove)
643                 endViewController.onLaunchAnimationStart(isExpandingFullyAbove)
644             }
645 
646             override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
647                 startViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
648                 endViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
649 
650                 onLaunchAnimationEnd()
651             }
652 
653             override fun onLaunchAnimationProgress(
654                 state: LaunchAnimator.State,
655                 progress: Float,
656                 linearProgress: Float
657             ) {
658                 startViewController.onLaunchAnimationProgress(state, progress, linearProgress)
659 
660                 // The end view is visible only iff the starting view is not visible.
661                 state.visible = !state.visible
662                 endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
663 
664                 // If the dialog content is complex, its dimension might change during the launch
665                 // animation. The animation end position might also change during the exit
666                 // animation, for instance when locking the phone when the dialog is open. Therefore
667                 // we update the end state to the new position/size. Usually the dialog dimension or
668                 // position will change in the early frames, so changing the end state shouldn't
669                 // really be noticeable.
670                 endViewController.fillGhostedViewState(endState)
671             }
672         }
673 
674         launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
675     }
676 
677     private fun shouldAnimateDialogIntoView(): Boolean {
678         // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit
679         // animation.
680         if (exitAnimationDisabled || !dialog.isShowing) {
681             return false
682         }
683 
684         // If we are dreaming, the dialog was probably closed because of that so we don't animate
685         // into the touchSurface.
686         if (dreamManager.isDreaming) {
687             return false
688         }
689 
690         // The touch surface should be invisible by now, if it's not then something else changed its
691         // visibility and we probably don't want to run the animation.
692         if (touchSurface.visibility != View.INVISIBLE) {
693             return false
694         }
695 
696         // If the touch surface is not attached or one of its ancestors is not visible, then we
697         // don't run the animation either.
698         if (!touchSurface.isAttachedToWindow) {
699             return false
700         }
701 
702         return (touchSurface.parent as? View)?.isShown ?: true
703     }
704 
705     /** A layout listener to animate the change of bounds of the dialog background.  */
706     class AnimatedBoundsLayoutListener : View.OnLayoutChangeListener {
707         companion object {
708             private const val ANIMATION_DURATION = 500L
709         }
710 
711         private var lastBounds: Rect? = null
712         private var currentAnimator: ValueAnimator? = null
713 
714         override fun onLayoutChange(
715             view: View,
716             left: Int,
717             top: Int,
718             right: Int,
719             bottom: Int,
720             oldLeft: Int,
721             oldTop: Int,
722             oldRight: Int,
723             oldBottom: Int
724         ) {
725             // Don't animate if bounds didn't actually change.
726             if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) {
727                 // Make sure that we that the last bounds set by the animator were not overridden.
728                 lastBounds?.let { bounds ->
729                     view.left = bounds.left
730                     view.top = bounds.top
731                     view.right = bounds.right
732                     view.bottom = bounds.bottom
733                 }
734                 return
735             }
736 
737             if (lastBounds == null) {
738                 lastBounds = Rect(oldLeft, oldTop, oldRight, oldBottom)
739             }
740 
741             val bounds = lastBounds!!
742             val startLeft = bounds.left
743             val startTop = bounds.top
744             val startRight = bounds.right
745             val startBottom = bounds.bottom
746 
747             currentAnimator?.cancel()
748             currentAnimator = null
749 
750             val animator = ValueAnimator.ofFloat(0f, 1f).apply {
751                 duration = ANIMATION_DURATION
752                 interpolator = Interpolators.STANDARD
753 
754                 addListener(object : AnimatorListenerAdapter() {
755                     override fun onAnimationEnd(animation: Animator) {
756                         currentAnimator = null
757                     }
758                 })
759 
760                 addUpdateListener { animatedValue ->
761                     val progress = animatedValue.animatedFraction
762 
763                     // Compute new bounds.
764                     bounds.left = MathUtils.lerp(startLeft, left, progress).roundToInt()
765                     bounds.top = MathUtils.lerp(startTop, top, progress).roundToInt()
766                     bounds.right = MathUtils.lerp(startRight, right, progress).roundToInt()
767                     bounds.bottom = MathUtils.lerp(startBottom, bottom, progress).roundToInt()
768 
769                     // Set the new bounds.
770                     view.left = bounds.left
771                     view.top = bounds.top
772                     view.right = bounds.right
773                     view.bottom = bounds.bottom
774                 }
775             }
776 
777             currentAnimator = animator
778             animator.start()
779         }
780     }
781 
782     fun prepareForStackDismiss(): View {
783         if (parentAnimatedDialog == null) {
784             return touchSurface
785         }
786         parentAnimatedDialog.exitAnimationDisabled = true
787         parentAnimatedDialog.dialog.hide()
788         val view = parentAnimatedDialog.prepareForStackDismiss()
789         parentAnimatedDialog.dialog.dismiss()
790         // Make the touch surface invisible, so we end up animating to it when we actually
791         // dismiss the stack
792         view.visibility = View.INVISIBLE
793         return view
794     }
795 }
796