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