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