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