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.app.Dialog 23 import android.graphics.Color 24 import android.graphics.Rect 25 import android.os.Looper 26 import android.util.Log 27 import android.util.MathUtils 28 import android.view.View 29 import android.view.ViewGroup 30 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 31 import android.view.ViewRootImpl 32 import android.view.WindowInsets 33 import android.view.WindowManager 34 import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 35 import com.android.app.animation.Interpolators 36 import com.android.internal.jank.Cuj.CujType 37 import com.android.internal.jank.InteractionJankMonitor 38 import com.android.systemui.Flags 39 import com.android.systemui.util.maybeForceFullscreen 40 import com.android.systemui.util.registerAnimationOnBackInvoked 41 import java.util.concurrent.Executor 42 import kotlin.math.roundToInt 43 44 private const val TAG = "DialogTransitionAnimator" 45 46 /** 47 * A class that allows dialogs to be started in a seamless way from a view that is transforming 48 * nicely into the starting dialog. 49 * 50 * This animator also allows to easily animate a dialog into an activity. 51 * 52 * @see show 53 * @see showFromView 54 * @see showFromDialog 55 * @see createActivityTransitionController 56 */ 57 class DialogTransitionAnimator 58 @JvmOverloads 59 constructor( 60 private val mainExecutor: Executor, 61 private val callback: Callback, 62 private val interactionJankMonitor: InteractionJankMonitor, 63 private val transitionAnimator: TransitionAnimator = 64 TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS), 65 private val isForTesting: Boolean = false, 66 ) { 67 private companion object { 68 private val TIMINGS = ActivityTransitionAnimator.TIMINGS 69 70 // We use the same interpolator for X and Y axis to make sure the dialog does not move out 71 // of the screen bounds during the animation. 72 private val INTERPOLATORS = 73 ActivityTransitionAnimator.INTERPOLATORS.copy( 74 positionXInterpolator = 75 ActivityTransitionAnimator.INTERPOLATORS.positionInterpolator 76 ) 77 } 78 79 /** 80 * A controller that takes care of applying the dialog launch and exit animations to the source 81 * that triggered the animation. 82 */ 83 interface Controller { 84 /** The [ViewRootImpl] of this controller. */ 85 val viewRoot: ViewRootImpl? 86 87 /** 88 * The identity object of the source animated by this controller. This animator will ensure 89 * that 2 animations with the same source identity are not going to run at the same time, to 90 * avoid flickers when a dialog is shown from the same source more or less at the same time 91 * (for instance if the user clicks an expandable button twice). 92 */ 93 val sourceIdentity: Any 94 95 /** The CUJ associated to this controller. */ 96 val cuj: DialogCuj? 97 98 /** 99 * Move the drawing of the source in the overlay of [viewGroup]. 100 * 101 * Once this method is called, and until [stopDrawingInOverlay] is called, the source 102 * controlled by this Controller should be drawn in the overlay of [viewGroup] so that it is 103 * drawn above all other elements in the same [viewRoot]. 104 */ 105 fun startDrawingInOverlayOf(viewGroup: ViewGroup) 106 107 /** 108 * Move the drawing of the source back in its original location. 109 * 110 * @see startDrawingInOverlayOf 111 */ 112 fun stopDrawingInOverlay() 113 114 /** 115 * Create the [TransitionAnimator.Controller] that will be called to animate the source 116 * controlled by this [Controller] during the dialog launch animation. 117 * 118 * At the end of this animation, the source should *not* be visible anymore (until the 119 * dialog is closed and is animated back into the source). 120 */ 121 fun createTransitionController(): TransitionAnimator.Controller 122 123 /** 124 * Create the [TransitionAnimator.Controller] that will be called to animate the source 125 * controlled by this [Controller] during the dialog exit animation. 126 * 127 * At the end of this animation, the source should be visible again. 128 */ 129 fun createExitController(): TransitionAnimator.Controller 130 131 /** 132 * Whether we should animate the dialog back into the source when it is dismissed. If this 133 * methods returns `false`, then the dialog will simply fade out and 134 * [onExitAnimationCancelled] will be called. 135 * 136 * Note that even when this returns `true`, the exit animation might still be cancelled (in 137 * which case [onExitAnimationCancelled] will also be called). 138 */ 139 fun shouldAnimateExit(): Boolean 140 141 /** 142 * Called if we decided to *not* animate the dialog into the source for some reason. This 143 * means that [createExitController] will *not* be called and this implementation should 144 * make sure that the source is back in its original state, before it was animated into the 145 * dialog. In particular, the source should be visible again. 146 */ 147 fun onExitAnimationCancelled() 148 149 /** 150 * Return the [InteractionJankMonitor.Configuration.Builder] to be used for animations 151 * controlled by this controller. 152 */ 153 // TODO(b/252723237): Make this non-nullable 154 fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? 155 156 companion object { 157 /** 158 * Create a [Controller] that can animate [source] to and from a dialog. 159 * 160 * Important: The view must be attached to a [ViewGroup] when calling this function and 161 * during the animation. For safety, this method will return null when it is not. The 162 * view must also implement [LaunchableView], otherwise this method will throw. 163 * 164 * Note: The background of [view] should be a (rounded) rectangle so that it can be 165 * properly animated. 166 */ 167 fun fromView(source: View, cuj: DialogCuj? = null): Controller? { 168 // Make sure the View we launch from implements LaunchableView to avoid visibility 169 // issues. 170 if (source !is LaunchableView) { 171 throw IllegalArgumentException( 172 "A DialogTransitionAnimator.Controller was created from a View that does " + 173 "not implement LaunchableView. This can lead to subtle bugs where " + 174 "the visibility of the View we are launching from is not what we " + 175 "expected." 176 ) 177 } 178 179 if (source.parent !is ViewGroup) { 180 Log.e( 181 TAG, 182 "Skipping animation as view $source is not attached to a ViewGroup", 183 Exception(), 184 ) 185 return null 186 } 187 188 return ViewDialogTransitionAnimatorController(source, cuj) 189 } 190 } 191 } 192 193 /** 194 * The set of dialogs that were animated using this animator and that are still opened (not 195 * dismissed, but can be hidden). 196 */ 197 // TODO(b/201264644): Remove this set. 198 private val openedDialogs = hashSetOf<AnimatedDialog>() 199 200 /** 201 * Show [dialog] by expanding it from [view]. If [view] is a view inside another dialog that was 202 * shown using this method, then we will animate from that dialog instead. 203 * 204 * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be 205 * animated when the dialog bounds change. 206 * 207 * Note: The background of [view] should be a (rounded) rectangle so that it can be properly 208 * animated. 209 * 210 * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be 211 * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. 212 */ 213 @JvmOverloads 214 fun showFromView( 215 dialog: Dialog, 216 view: View, 217 cuj: DialogCuj? = null, 218 animateBackgroundBoundsChange: Boolean = false, 219 ) { 220 val controller = Controller.fromView(view, cuj) 221 if (controller == null) { 222 dialog.show() 223 } else { 224 show(dialog, controller, animateBackgroundBoundsChange) 225 } 226 } 227 228 /** 229 * Show [dialog] by expanding it from a source controlled by [controller]. 230 * 231 * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be 232 * animated when the dialog bounds change. 233 * 234 * Note: The background of [view] should be a (rounded) rectangle so that it can be properly 235 * animated. 236 * 237 * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be 238 * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. 239 */ 240 @JvmOverloads 241 fun show( 242 dialog: Dialog, 243 controller: Controller, 244 animateBackgroundBoundsChange: Boolean = false, 245 ) { 246 if (Looper.myLooper() != Looper.getMainLooper()) { 247 throw IllegalStateException( 248 "showFromView must be called from the main thread and dialog must be created in " + 249 "the main thread" 250 ) 251 } 252 253 // If the view we are launching from belongs to another dialog, then this means the caller 254 // intent is to launch a dialog from another dialog. 255 val animatedParent = 256 openedDialogs.firstOrNull { 257 it.dialog.window?.decorView?.viewRootImpl == controller.viewRoot 258 } 259 val controller = 260 animatedParent?.dialogContentWithBackground?.let { 261 Controller.fromView(it, controller.cuj) 262 } ?: controller 263 264 // Make sure we don't run the launch animation from the same source twice at the same time. 265 if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) { 266 Log.e( 267 TAG, 268 "Not running dialog launch animation from source as it is already expanded into a" + 269 " dialog", 270 ) 271 dialog.show() 272 return 273 } 274 275 val animatedDialog = 276 AnimatedDialog( 277 transitionAnimator = transitionAnimator, 278 callback = callback, 279 interactionJankMonitor = interactionJankMonitor, 280 controller = controller, 281 onDialogDismissed = { openedDialogs.remove(it) }, 282 dialog = dialog, 283 animateBackgroundBoundsChange = animateBackgroundBoundsChange, 284 parentAnimatedDialog = animatedParent, 285 forceDisableSynchronization = isForTesting, 286 ) 287 288 openedDialogs.add(animatedDialog) 289 animatedDialog.start() 290 } 291 292 /** 293 * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will 294 * allow for dismissing the whole stack. 295 * 296 * @see dismissStack 297 */ 298 fun showFromDialog( 299 dialog: Dialog, 300 animateFrom: Dialog, 301 cuj: DialogCuj? = null, 302 animateBackgroundBoundsChange: Boolean = false, 303 ) { 304 val view = 305 openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground 306 if (view == null) { 307 Log.w( 308 TAG, 309 "Showing dialog $dialog normally as the dialog it is shown from was not shown " + 310 "using DialogTransitionAnimator", 311 ) 312 dialog.show() 313 return 314 } 315 316 showFromView( 317 dialog, 318 view, 319 animateBackgroundBoundsChange = animateBackgroundBoundsChange, 320 cuj = cuj, 321 ) 322 } 323 324 /** 325 * Create an [ActivityTransitionAnimator.Controller] that can be used to launch an activity from 326 * the dialog that contains [View]. Note that the dialog must have been shown using this 327 * animator, otherwise this method will return null. 328 * 329 * The returned controller will take care of dismissing the dialog at the right time after the 330 * activity started, when the dialog to app animation is done (or when it is cancelled). If this 331 * method returns null, then the dialog won't be dismissed. 332 * 333 * @param view any view inside the dialog to animate. 334 */ 335 @JvmOverloads 336 fun createActivityTransitionController( 337 view: View, 338 cujType: Int? = null, 339 ): ActivityTransitionAnimator.Controller? { 340 val animatedDialog = 341 openedDialogs.firstOrNull { 342 it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl 343 } ?: return null 344 return createActivityTransitionController(animatedDialog, cujType) 345 } 346 347 /** 348 * Create an [ActivityTransitionAnimator.Controller] that can be used to launch an activity from 349 * [dialog]. Note that the dialog must have been shown using this animator, otherwise this 350 * method will return null. 351 * 352 * The returned controller will take care of dismissing the dialog at the right time after the 353 * activity started, when the dialog to app animation is done (or when it is cancelled). If this 354 * method returns null, then the dialog won't be dismissed. 355 * 356 * @param dialog the dialog to animate. 357 */ 358 @JvmOverloads 359 fun createActivityTransitionController( 360 dialog: Dialog, 361 cujType: Int? = null, 362 ): ActivityTransitionAnimator.Controller? { 363 val animatedDialog = openedDialogs.firstOrNull { it.dialog == dialog } ?: return null 364 return createActivityTransitionController(animatedDialog, cujType) 365 } 366 367 private fun createActivityTransitionController( 368 animatedDialog: AnimatedDialog, 369 cujType: Int? = null, 370 ): ActivityTransitionAnimator.Controller? { 371 // At this point, we know that the intent of the caller is to dismiss the dialog to show 372 // an app, so we disable the exit animation into the source because we will never want to 373 // run it anyways. 374 animatedDialog.exitAnimationDisabled = true 375 376 val dialog = animatedDialog.dialog 377 378 // Don't animate if the dialog is not showing or if we are locked and going to show the 379 // primary bouncer. 380 if ( 381 !dialog.isShowing || 382 (!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock()) 383 ) { 384 return null 385 } 386 387 val dialogContentWithBackground = animatedDialog.dialogContentWithBackground ?: return null 388 val controller = 389 ActivityTransitionAnimator.Controller.fromView(dialogContentWithBackground, cujType) 390 ?: return null 391 392 // Wrap the controller into one that will instantly dismiss the dialog when the animation is 393 // done or dismiss it normally (fading it out) if the animation is cancelled. 394 return object : ActivityTransitionAnimator.Controller by controller { 395 override val isDialogLaunch = true 396 397 override fun onIntentStarted(willAnimate: Boolean) { 398 controller.onIntentStarted(willAnimate) 399 400 if (!willAnimate) { 401 dialog.dismiss() 402 } 403 } 404 405 override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { 406 controller.onTransitionAnimationCancelled() 407 enableDialogDismiss() 408 dialog.dismiss() 409 } 410 411 override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { 412 controller.onTransitionAnimationStart(isExpandingFullyAbove) 413 414 // Make sure the dialog is not dismissed during the animation. 415 disableDialogDismiss() 416 417 // If this dialog was shown from a cascade of other dialogs, make sure those ones 418 // are dismissed too. 419 animatedDialog.prepareForStackDismiss() 420 421 // Remove the dim. 422 dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) 423 } 424 425 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { 426 controller.onTransitionAnimationEnd(isExpandingFullyAbove) 427 428 // Hide the dialog then dismiss it to instantly dismiss it without playing the 429 // animation. 430 dialog.hide() 431 enableDialogDismiss() 432 dialog.dismiss() 433 } 434 435 private fun disableDialogDismiss() { 436 dialog.setDismissOverride { /* Do nothing */ } 437 } 438 439 private fun enableDialogDismiss() { 440 // We don't set the override to null given that [AnimatedDialog.OnDialogDismissed] 441 // will still properly dismiss the dialog but will also make sure to clean up 442 // everything (like making sure that the touched view that triggered the dialog is 443 // made VISIBLE again). 444 dialog.setDismissOverride(animatedDialog::onDialogDismissed) 445 } 446 } 447 } 448 449 /** 450 * Ensure that all dialogs currently shown won't animate into their source when dismissed. 451 * 452 * This is a temporary API meant to be called right before we both dismiss a dialog and start an 453 * activity, which currently does not look good if we animate the dialog into their source at 454 * the same time as the activity starts. 455 * 456 * TODO(b/193634619): Remove this function and animate dialog into opening activity instead. 457 */ 458 fun disableAllCurrentDialogsExitAnimations() { 459 openedDialogs.forEach { it.exitAnimationDisabled = true } 460 } 461 462 /** 463 * Dismiss [dialog]. If it was launched from another dialog using this animator, also dismiss 464 * the stack of dialogs and simply fade out [dialog]. 465 */ 466 fun dismissStack(dialog: Dialog) { 467 openedDialogs.firstOrNull { it.dialog == dialog }?.prepareForStackDismiss() 468 dialog.dismiss() 469 } 470 471 interface Callback { 472 /** Whether the device is currently in dreaming (screensaver) mode. */ 473 fun isDreaming(): Boolean 474 475 /** 476 * Whether the device is currently unlocked, i.e. if it is *not* on the keyguard or if the 477 * keyguard can be dismissed. 478 */ 479 fun isUnlocked(): Boolean 480 481 /** 482 * Whether we are going to show alternate authentication (like UDFPS) instead of the 483 * traditional bouncer when unlocking the device. 484 */ 485 fun isShowingAlternateAuthOnUnlock(): Boolean 486 } 487 } 488 489 /** 490 * The CUJ interaction associated with opening the dialog. 491 * 492 * The optional tag indicates the specific dialog being opened. 493 */ 494 data class DialogCuj(@CujType val cujType: Int, val tag: String? = null) 495 496 private class AnimatedDialog( 497 private val transitionAnimator: TransitionAnimator, 498 private val callback: DialogTransitionAnimator.Callback, 499 private val interactionJankMonitor: InteractionJankMonitor, 500 501 /** 502 * The controller of the source that triggered the dialog and that will animate into/from the 503 * dialog. 504 */ 505 val controller: DialogTransitionAnimator.Controller, 506 507 /** 508 * A callback that will be called with this [AnimatedDialog] after the dialog was dismissed and 509 * the exit animation is done. 510 */ 511 private val onDialogDismissed: (AnimatedDialog) -> Unit, 512 513 /** The dialog to show and animate. */ 514 val dialog: Dialog, 515 516 /** Whether we should animate the dialog background when its bounds change. */ 517 animateBackgroundBoundsChange: Boolean, 518 519 /** Launch animation corresponding to the parent [AnimatedDialog]. */ 520 private val parentAnimatedDialog: AnimatedDialog? = null, 521 522 /** 523 * Whether synchronization should be disabled, which can be useful if we are running in a test. 524 */ 525 private val forceDisableSynchronization: Boolean, 526 ) { 527 /** 528 * The DecorView of this dialog window. 529 * 530 * Note that we access this DecorView lazily to avoid accessing it before the dialog is created, 531 * which can sometimes cause crashes (e.g. with the Cast dialog). 532 */ <lambda>null533 private val decorView by lazy { dialog.window!!.decorView as ViewGroup } 534 535 /** 536 * The dialog content with its background. When animating a fullscreen dialog, this is just the 537 * first ViewGroup of the dialog that has a background. When animating a normal (not fullscreen) 538 * dialog, this is an additional view that serves as a fake window that will have the same size 539 * as the dialog window initially had and to which we will set the dialog window background. 540 */ 541 var dialogContentWithBackground: ViewGroup? = null 542 543 /** The background color of [dialog], taking into consideration its window background color. */ 544 private var originalDialogBackgroundColor = Color.BLACK 545 546 /** 547 * Whether we are currently launching/showing the dialog by animating it from its source 548 * controlled by [controller]. 549 */ 550 private var isLaunching = true 551 552 /** Whether we are currently dismissing/hiding the dialog by animating into its source. */ 553 private var isDismissing = false 554 555 private var dismissRequested = false 556 var exitAnimationDisabled = false 557 558 private var isSourceDrawnInDialog = false 559 private var isOriginalDialogViewLaidOut = false 560 561 /** A layout listener to animate the dialog height change. */ 562 private val backgroundLayoutListener = 563 if (animateBackgroundBoundsChange) { 564 AnimatedBoundsLayoutListener() 565 } else { 566 null 567 } 568 569 /* 570 * A layout listener in case the dialog (window) size changes (for instance because of a 571 * configuration change) to ensure that the dialog stays full width. 572 */ 573 private var decorViewLayoutListener: View.OnLayoutChangeListener? = null 574 575 private var hasInstrumentedJank = false 576 startnull577 fun start() { 578 val cuj = controller.cuj 579 if (cuj != null) { 580 val config = controller.jankConfigurationBuilder() 581 if (config != null) { 582 if (cuj.tag != null) { 583 config.setTag(cuj.tag) 584 } 585 586 interactionJankMonitor.begin(config) 587 hasInstrumentedJank = true 588 } 589 } 590 591 // Create the dialog so that its onCreate() method is called, which usually sets the dialog 592 // content. 593 dialog.create() 594 595 val window = dialog.window!! 596 val isWindowFullScreen = 597 window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT 598 val dialogContentWithBackground = 599 if (isWindowFullScreen) { 600 // If the dialog window is already fullscreen, then we look for the first ViewGroup 601 // that has a background (and is not the DecorView, which always has a background) 602 // and animate towards that ViewGroup given that this is probably what represents 603 // the actual dialog view. 604 var viewGroupWithBackground: ViewGroup? = null 605 for (i in 0 until decorView.childCount) { 606 viewGroupWithBackground = 607 findFirstViewGroupWithBackground(decorView.getChildAt(i)) 608 if (viewGroupWithBackground != null) { 609 break 610 } 611 } 612 613 // Animate that view with the background. Throw if we didn't find one, because 614 // otherwise it's not clear what we should animate. 615 if (viewGroupWithBackground == null) { 616 error("Unable to find ViewGroup with background") 617 } 618 619 if (viewGroupWithBackground !is LaunchableView) { 620 error("The animated ViewGroup with background must implement LaunchableView") 621 } 622 623 viewGroupWithBackground 624 } else { 625 val (dialogContentWithBackground, decorViewLayoutListener) = 626 dialog.maybeForceFullscreen()!! 627 this.decorViewLayoutListener = decorViewLayoutListener 628 dialogContentWithBackground 629 } 630 631 this.dialogContentWithBackground = dialogContentWithBackground 632 dialogContentWithBackground.setTag(R.id.tag_dialog_background, true) 633 634 val background = dialogContentWithBackground.background 635 originalDialogBackgroundColor = 636 GhostedViewTransitionAnimatorController.findGradientDrawable(background) 637 ?.color 638 ?.defaultColor ?: Color.BLACK 639 640 // Make the background view invisible until we start the animation. We use the transition 641 // visibility like GhostView does so that we don't mess up with the accessibility tree (see 642 // b/204944038#comment17). Given that this background implements LaunchableView, we call 643 // setShouldBlockVisibilityChanges() early so that the current visibility (VISIBLE) is 644 // restored at the end of the animation. 645 dialogContentWithBackground.setShouldBlockVisibilityChanges(true) 646 dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE) 647 648 // Make sure the dialog is visible instantly and does not do any window animation. 649 val attributes = window.attributes 650 attributes.windowAnimations = R.style.Animation_LaunchAnimation 651 652 // Ensure that the animation is not clipped by the display cut-out when animating this 653 // dialog into an app. 654 attributes.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 655 656 // Ensure that the animation is not clipped by the navigation/task bars when animating this 657 // dialog into an app. 658 val wasFittingNavigationBars = 659 attributes.fitInsetsTypes and WindowInsets.Type.navigationBars() != 0 660 attributes.fitInsetsTypes = 661 attributes.fitInsetsTypes and WindowInsets.Type.navigationBars().inv() 662 663 window.attributes = window.attributes 664 665 // We apply the insets ourselves to make sure that the paddings are set on the correct 666 // View. 667 window.setDecorFitsSystemWindows(false) 668 val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup) 669 viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets -> 670 val type = 671 if (wasFittingNavigationBars) { 672 WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars() 673 } else { 674 WindowInsets.Type.displayCutout() 675 } 676 677 val insets = windowInsets.getInsets(type) 678 view.setPadding(insets.left, insets.top, insets.right, insets.bottom) 679 WindowInsets.CONSUMED 680 } 681 682 // Start the animation once the background view is properly laid out. 683 dialogContentWithBackground.addOnLayoutChangeListener( 684 object : View.OnLayoutChangeListener { 685 override fun onLayoutChange( 686 v: View, 687 left: Int, 688 top: Int, 689 right: Int, 690 bottom: Int, 691 oldLeft: Int, 692 oldTop: Int, 693 oldRight: Int, 694 oldBottom: Int, 695 ) { 696 dialogContentWithBackground.removeOnLayoutChangeListener(this) 697 698 isOriginalDialogViewLaidOut = true 699 maybeStartLaunchAnimation() 700 } 701 } 702 ) 703 704 // Disable the dim. We will enable it once we start the animation. 705 window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) 706 707 // Override the dialog dismiss() so that we can animate the exit before actually dismissing 708 // the dialog. 709 dialog.setDismissOverride(this::onDialogDismissed) 710 711 dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground) 712 713 // Show the dialog. 714 dialog.show() 715 moveSourceDrawingToDialog() 716 } 717 moveSourceDrawingToDialognull718 private fun moveSourceDrawingToDialog() { 719 if (decorView.viewRootImpl == null) { 720 // Make sure that we have access to the dialog view root to move the drawing to the 721 // dialog overlay. 722 decorView.post(::moveSourceDrawingToDialog) 723 return 724 } 725 726 // Move the drawing of the source in the overlay of this dialog, then animate. We trigger a 727 // one-off synchronization to make sure that this is done in sync between the two different 728 // windows. 729 controller.startDrawingInOverlayOf(decorView) 730 synchronizeNextDraw( 731 then = { 732 isSourceDrawnInDialog = true 733 maybeStartLaunchAnimation() 734 } 735 ) 736 } 737 738 /** 739 * Synchronize the next draw of the source and dialog view roots so that they are performed at 740 * the same time, in the same transaction. This is necessary to make sure that the source is 741 * drawn in the overlay at the same time as it is removed from its original position (or 742 * inversely, removed from the overlay when the source is moved back to its original position). 743 */ synchronizeNextDrawnull744 private fun synchronizeNextDraw(then: () -> Unit) { 745 val controllerRootView = controller.viewRoot?.view 746 if (forceDisableSynchronization || controllerRootView == null) { 747 // Don't synchronize when inside an automated test or if the controller root view is 748 // detached. 749 then() 750 return 751 } 752 753 ViewRootSync.synchronizeNextDraw(controllerRootView, decorView, then) 754 decorView.invalidate() 755 controllerRootView.invalidate() 756 } 757 findFirstViewGroupWithBackgroundnull758 private fun findFirstViewGroupWithBackground(view: View): ViewGroup? { 759 if (view !is ViewGroup) { 760 return null 761 } 762 763 if (view.background != null) { 764 return view 765 } 766 767 for (i in 0 until view.childCount) { 768 val match = findFirstViewGroupWithBackground(view.getChildAt(i)) 769 if (match != null) { 770 return match 771 } 772 } 773 774 return null 775 } 776 maybeStartLaunchAnimationnull777 private fun maybeStartLaunchAnimation() { 778 if (!isSourceDrawnInDialog || !isOriginalDialogViewLaidOut) { 779 return 780 } 781 782 // Show the background dim. 783 dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) 784 785 startAnimation( 786 isLaunching = true, 787 onLaunchAnimationEnd = { 788 isLaunching = false 789 790 // dismiss was called during the animation, dismiss again now to actually dismiss. 791 if (dismissRequested) { 792 dialog.dismiss() 793 } 794 795 // If necessary, we animate the dialog background when its bounds change. We do it 796 // at the end of the launch animation, because the lauch animation already correctly 797 // handles bounds changes. 798 if (backgroundLayoutListener != null) { 799 dialogContentWithBackground!!.addOnLayoutChangeListener( 800 backgroundLayoutListener 801 ) 802 } 803 804 if (hasInstrumentedJank) { 805 interactionJankMonitor.end(controller.cuj!!.cujType) 806 } 807 }, 808 ) 809 } 810 onDialogDismissednull811 fun onDialogDismissed() { 812 if (Looper.myLooper() != Looper.getMainLooper()) { 813 dialog.context.mainExecutor.execute { onDialogDismissed() } 814 return 815 } 816 817 // TODO(b/193634619): Support interrupting the launch animation in the middle. 818 if (isLaunching) { 819 dismissRequested = true 820 return 821 } 822 823 if (isDismissing) { 824 return 825 } 826 827 isDismissing = true 828 hideDialogIntoView { animationRan: Boolean -> 829 if (animationRan) { 830 // Instantly dismiss the dialog if we ran the animation into view. If it was 831 // skipped, dismiss() will run the window animation (which fades out the dialog). 832 dialog.hide() 833 } 834 835 dialog.setDismissOverride(null) 836 dialog.dismiss() 837 } 838 } 839 840 /** 841 * Hide the dialog into the source and call [onAnimationFinished] when the animation is done 842 * (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually 843 * dismiss the dialog. 844 */ hideDialogIntoViewnull845 private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) { 846 // Remove the layout change listener we have added to the DecorView earlier. 847 if (decorViewLayoutListener != null) { 848 decorView.removeOnLayoutChangeListener(decorViewLayoutListener) 849 } 850 851 if (!shouldAnimateDialogIntoSource()) { 852 Log.i(TAG, "Skipping animation of dialog into the source") 853 controller.onExitAnimationCancelled() 854 onAnimationFinished(false /* instantDismiss */) 855 onDialogDismissed(this@AnimatedDialog) 856 return 857 } 858 859 startAnimation( 860 isLaunching = false, 861 onLaunchAnimationStart = { 862 // Remove the dim background as soon as we start the animation. 863 dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) 864 }, 865 onLaunchAnimationEnd = { 866 val dialogContentWithBackground = this.dialogContentWithBackground!! 867 dialogContentWithBackground.visibility = View.INVISIBLE 868 869 if (backgroundLayoutListener != null) { 870 dialogContentWithBackground.removeOnLayoutChangeListener( 871 backgroundLayoutListener 872 ) 873 } 874 875 controller.stopDrawingInOverlay() 876 synchronizeNextDraw { 877 onAnimationFinished(true /* instantDismiss */) 878 onDialogDismissed(this@AnimatedDialog) 879 } 880 }, 881 ) 882 } 883 startAnimationnull884 private fun startAnimation( 885 isLaunching: Boolean, 886 onLaunchAnimationStart: () -> Unit = {}, <lambda>null887 onLaunchAnimationEnd: () -> Unit = {}, 888 ) { 889 // Create 2 controllers to animate both the dialog and the source. 890 val startController = 891 if (isLaunching) { 892 controller.createTransitionController() 893 } else { 894 GhostedViewTransitionAnimatorController(dialogContentWithBackground!!) 895 } 896 val endController = 897 if (isLaunching) { 898 GhostedViewTransitionAnimatorController(dialogContentWithBackground!!) 899 } else { 900 controller.createExitController() 901 } 902 startController.transitionContainer = decorView 903 endController.transitionContainer = decorView 904 905 val endState = endController.createAnimatorState() 906 val controller = 907 object : TransitionAnimator.Controller { 908 override var transitionContainer: ViewGroup 909 get() = startController.transitionContainer 910 set(value) { 911 startController.transitionContainer = value 912 endController.transitionContainer = value 913 } 914 915 // We tell TransitionController that this is always a launch, and handle the launch 916 // vs return logic internally. 917 // TODO(b/323863002): maybe move the launch vs return logic out of this class and 918 // delegate it to TransitionController? 919 override val isLaunching: Boolean = true 920 createAnimatorStatenull921 override fun createAnimatorState(): TransitionAnimator.State { 922 return startController.createAnimatorState() 923 } 924 onTransitionAnimationStartnull925 override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { 926 // During launch, onLaunchAnimationStart will be used to remove the temporary 927 // touch surface ghost so it is important to call this before calling 928 // onLaunchAnimationStart on the controller (which will create its own ghost). 929 onLaunchAnimationStart() 930 931 startController.onTransitionAnimationStart(isExpandingFullyAbove) 932 endController.onTransitionAnimationStart(isExpandingFullyAbove) 933 } 934 onTransitionAnimationEndnull935 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { 936 val onEnd = { 937 startController.onTransitionAnimationEnd(isExpandingFullyAbove) 938 endController.onTransitionAnimationEnd(isExpandingFullyAbove) 939 onLaunchAnimationEnd() 940 } 941 if (Flags.sceneContainer()) { 942 onEnd() 943 } else { 944 // onLaunchAnimationEnd is called by an Animator at the end of the 945 // animation, on a Choreographer animation tick. The following calls will 946 // move the animated content from the dialog overlay back to its original 947 // position, and this change must be reflected in the next frame given that 948 // we then sync the next frame of both the content and dialog ViewRoots. 949 // However, in case that content is rendered by Compose, whose compositions 950 // are also scheduled on a Choreographer frame, any state change made *right 951 // now* won't be reflected in the next frame given that a Choreographer 952 // frame can't schedule another and have it happen in the same frame. So we 953 // post the forwarded calls to [Controller.onLaunchAnimationEnd], leaving 954 // this Choreographer frame, ensuring that the move of the content back to 955 // its original window will be reflected in the next frame right after 956 // [onLaunchAnimationEnd] is called. 957 dialog.context.mainExecutor.execute { onEnd() } 958 } 959 } 960 onTransitionAnimationProgressnull961 override fun onTransitionAnimationProgress( 962 state: TransitionAnimator.State, 963 progress: Float, 964 linearProgress: Float, 965 ) { 966 startController.onTransitionAnimationProgress(state, progress, linearProgress) 967 968 // The end view is visible only iff the starting view is not visible. 969 state.visible = !state.visible 970 endController.onTransitionAnimationProgress(state, progress, linearProgress) 971 972 // If the dialog content is complex, its dimension might change during the 973 // launch animation. The animation end position might also change during the 974 // exit animation, for instance when locking the phone when the dialog is open. 975 // Therefore we update the end state to the new position/size. Usually the 976 // dialog dimension or position will change in the early frames, so changing the 977 // end state shouldn't really be noticeable. 978 if (endController is GhostedViewTransitionAnimatorController) { 979 endController.fillGhostedViewState(endState) 980 } 981 } 982 } 983 984 transitionAnimator.startAnimation(controller, endState, originalDialogBackgroundColor) 985 } 986 shouldAnimateDialogIntoSourcenull987 private fun shouldAnimateDialogIntoSource(): Boolean { 988 // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit 989 // animation. 990 if (exitAnimationDisabled || !dialog.isShowing) { 991 return false 992 } 993 994 // If we are dreaming, the dialog was probably closed because of that so we don't animate 995 // into the source. 996 if (callback.isDreaming()) { 997 return false 998 } 999 1000 return controller.shouldAnimateExit() 1001 } 1002 1003 /** A layout listener to animate the change of bounds of the dialog background. */ 1004 class AnimatedBoundsLayoutListener : View.OnLayoutChangeListener { 1005 companion object { 1006 private const val ANIMATION_DURATION = 500L 1007 } 1008 1009 private var lastBounds: Rect? = null 1010 private var currentAnimator: ValueAnimator? = null 1011 onLayoutChangenull1012 override fun onLayoutChange( 1013 view: View, 1014 left: Int, 1015 top: Int, 1016 right: Int, 1017 bottom: Int, 1018 oldLeft: Int, 1019 oldTop: Int, 1020 oldRight: Int, 1021 oldBottom: Int, 1022 ) { 1023 // Don't animate if bounds didn't actually change. 1024 if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) { 1025 // Make sure that we that the last bounds set by the animator were not overridden. 1026 lastBounds?.let { bounds -> 1027 view.left = bounds.left 1028 view.top = bounds.top 1029 view.right = bounds.right 1030 view.bottom = bounds.bottom 1031 } 1032 return 1033 } 1034 1035 if (lastBounds == null) { 1036 lastBounds = Rect(oldLeft, oldTop, oldRight, oldBottom) 1037 } 1038 1039 val bounds = lastBounds!! 1040 val startLeft = bounds.left 1041 val startTop = bounds.top 1042 val startRight = bounds.right 1043 val startBottom = bounds.bottom 1044 1045 currentAnimator?.cancel() 1046 currentAnimator = null 1047 1048 val animator = 1049 ValueAnimator.ofFloat(0f, 1f).apply { 1050 duration = ANIMATION_DURATION 1051 interpolator = Interpolators.STANDARD 1052 1053 addListener( 1054 object : AnimatorListenerAdapter() { 1055 override fun onAnimationEnd(animation: Animator) { 1056 currentAnimator = null 1057 } 1058 } 1059 ) 1060 1061 addUpdateListener { animatedValue -> 1062 val progress = animatedValue.animatedFraction 1063 1064 // Compute new bounds. 1065 bounds.left = MathUtils.lerp(startLeft, left, progress).roundToInt() 1066 bounds.top = MathUtils.lerp(startTop, top, progress).roundToInt() 1067 bounds.right = MathUtils.lerp(startRight, right, progress).roundToInt() 1068 bounds.bottom = MathUtils.lerp(startBottom, bottom, progress).roundToInt() 1069 1070 // Set the new bounds. 1071 view.left = bounds.left 1072 view.top = bounds.top 1073 view.right = bounds.right 1074 view.bottom = bounds.bottom 1075 } 1076 } 1077 1078 currentAnimator = animator 1079 animator.start() 1080 } 1081 } 1082 prepareForStackDismissnull1083 fun prepareForStackDismiss() { 1084 if (parentAnimatedDialog == null) { 1085 return 1086 } 1087 parentAnimatedDialog.exitAnimationDisabled = true 1088 parentAnimatedDialog.dialog.hide() 1089 parentAnimatedDialog.prepareForStackDismiss() 1090 parentAnimatedDialog.dialog.dismiss() 1091 } 1092 } 1093