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.content.ComponentName 20 import android.graphics.Canvas 21 import android.graphics.ColorFilter 22 import android.graphics.Insets 23 import android.graphics.Matrix 24 import android.graphics.PixelFormat 25 import android.graphics.Rect 26 import android.graphics.drawable.Drawable 27 import android.graphics.drawable.GradientDrawable 28 import android.graphics.drawable.InsetDrawable 29 import android.graphics.drawable.LayerDrawable 30 import android.graphics.drawable.StateListDrawable 31 import android.util.Log 32 import android.view.GhostView 33 import android.view.View 34 import android.view.ViewGroup 35 import android.view.ViewGroupOverlay 36 import android.widget.FrameLayout 37 import com.android.internal.jank.Cuj.CujType 38 import com.android.internal.jank.InteractionJankMonitor 39 import com.android.systemui.Flags 40 import java.util.LinkedList 41 import kotlin.math.min 42 import kotlin.math.roundToInt 43 44 private const val TAG = "GhostedViewTransitionAnimatorController" 45 46 /** 47 * A base implementation of [ActivityTransitionAnimator.Controller] which creates a 48 * [ghost][GhostView] of [ghostedView] as well as an expandable background view, which are drawn and 49 * animated instead of the ghosted view. 50 * 51 * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during 52 * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown 53 * during this controller instantiation. 54 * 55 * Note: Avoid instantiating this directly and call [ActivityTransitionAnimator.Controller.fromView] 56 * whenever possible instead. 57 */ 58 open class GhostedViewTransitionAnimatorController 59 @JvmOverloads 60 constructor( 61 /** The view that will be ghosted and from which the background will be extracted. */ 62 transitioningView: View, 63 64 /** The [CujType] associated to this launch animation. */ 65 private val launchCujType: Int? = null, 66 override val transitionCookie: ActivityTransitionAnimator.TransitionCookie? = null, 67 override val component: ComponentName? = null, 68 69 /** The [CujType] associated to this return animation. */ 70 private val returnCujType: Int? = null, 71 72 /** 73 * Whether this controller should be invalidated after its first use, and whenever [ghostedView] 74 * is detached. 75 */ 76 private val isEphemeral: Boolean = false, 77 private var interactionJankMonitor: InteractionJankMonitor = 78 InteractionJankMonitor.getInstance(), 79 80 /** [ViewTransitionRegistry] to store the mapping of transitioning view and its token */ 81 private val transitionRegistry: IViewTransitionRegistry? = 82 if (Flags.decoupleViewControllerInAnimlib()) { 83 ViewTransitionRegistry.instance 84 } else { 85 null 86 }, 87 ) : ActivityTransitionAnimator.Controller { 88 override val isLaunching: Boolean = true 89 90 /** The container to which we will add the ghost view and expanding background. */ 91 override var transitionContainer: ViewGroup 92 get() = ghostedView.rootView as ViewGroup 93 set(_) { 94 // empty, should never be set to avoid memory leak 95 } 96 97 private val transitionContainerOverlay: ViewGroupOverlay 98 get() = transitionContainer.overlay 99 100 private val transitionContainerLocation = IntArray(2) 101 102 /** The ghost view that is drawn and animated instead of the ghosted view. */ 103 private var ghostView: GhostView? = null <lambda>null104 private val initialGhostViewMatrixValues = FloatArray(9) { 0f } 105 private val ghostViewMatrix = Matrix() 106 107 /** 108 * The expanding background view that will be added to [transitionContainer] (below [ghostView]) 109 * and animate. 110 */ 111 private var backgroundView: FrameLayout? = null 112 113 /** 114 * The drawable wrapping the [ghostedView] background and used as background for 115 * [backgroundView]. 116 */ 117 private var backgroundDrawable: WrappedDrawable? = null <lambda>null118 private val backgroundInsets by lazy { background?.opticalInsets ?: Insets.NONE } 119 private var startBackgroundAlpha: Int = 0xFF 120 121 private val ghostedViewLocation = IntArray(2) 122 private val ghostedViewState = TransitionAnimator.State() 123 124 /** 125 * The background of the [ghostedView]. This background will be used to draw the background of 126 * the background view that is expanding up to the final animation position. 127 * 128 * Note that during the animation, the alpha value value of this background will be set to 0, 129 * then set back to its initial value at the end of the animation. 130 */ 131 private val background: Drawable? 132 133 /** CUJ identifier accounting for whether this controller is for a launch or a return. */ 134 private val cujType: Int? 135 get() = 136 if (isLaunching) { 137 launchCujType 138 } else { 139 returnCujType 140 } 141 142 /** 143 * Used to automatically clean up the internal state once [ghostedView] is detached from the 144 * hierarchy. 145 */ 146 private val detachListener = 147 object : View.OnAttachStateChangeListener { onViewAttachedToWindownull148 override fun onViewAttachedToWindow(v: View) {} 149 onViewDetachedFromWindownull150 override fun onViewDetachedFromWindow(v: View) { 151 onDispose() 152 } 153 } 154 155 /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */ 156 private val transitionToken = 157 if (Flags.decoupleViewControllerInAnimlib()) { 158 transitionRegistry?.register(transitioningView) 159 } else { 160 null 161 } 162 163 /** The view that will be ghosted and from which the background will be extracted */ 164 private val ghostedView: View 165 get() = 166 if (Flags.decoupleViewControllerInAnimlib()) { tokennull167 transitionToken?.let { token -> transitionRegistry?.getView(token) } 168 } else { 169 _ghostedView 170 }!! 171 172 private val _ghostedView = 173 if (Flags.decoupleViewControllerInAnimlib()) { 174 null 175 } else { 176 transitioningView 177 } 178 179 init { 180 // Make sure the View we launch from implements LaunchableView to avoid visibility issues. 181 if (transitioningView !is LaunchableView) { 182 throw IllegalArgumentException( 183 "A GhostedViewLaunchAnimatorController was created from a View that does not " + 184 "implement LaunchableView. This can lead to subtle bugs where the visibility " + 185 "of the View we are launching from is not what we expected." 186 ) 187 } 188 189 /** Find the first view with a background in [view] and its children. */ findBackgroundnull190 fun findBackground(view: View): Drawable? { 191 if (view.background != null) { 192 return view.background 193 } 194 195 // Perform a BFS to find the largest View with background. 196 val views = LinkedList<View>().apply { add(view) } 197 198 while (views.isNotEmpty()) { 199 val v = views.removeAt(0) 200 if (v.background != null) { 201 return v.background 202 } 203 204 if (v is ViewGroup) { 205 for (i in 0 until v.childCount) { 206 views.add(v.getChildAt(i)) 207 } 208 } 209 } 210 211 return null 212 } 213 214 background = findBackground(ghostedView) 215 216 if (TransitionAnimator.returnAnimationsEnabled() && isEphemeral) { 217 ghostedView.addOnAttachStateChangeListener(detachListener) 218 } 219 } 220 onDisposenull221 override fun onDispose() { 222 if (TransitionAnimator.returnAnimationsEnabled()) { 223 ghostedView.removeOnAttachStateChangeListener(detachListener) 224 } 225 transitionToken?.let { token -> transitionRegistry?.unregister(token) } 226 } 227 228 /** 229 * Set the corner radius of [background]. The background is the one that was returned by 230 * [getBackground]. 231 */ setBackgroundCornerRadiusnull232 protected open fun setBackgroundCornerRadius( 233 background: Drawable, 234 topCornerRadius: Float, 235 bottomCornerRadius: Float, 236 ) { 237 // By default, we rely on WrappedDrawable to set/restore the background radii before/after 238 // each draw. 239 backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius) 240 } 241 242 /** Return the current top corner radius of the background. */ getCurrentTopCornerRadiusnull243 protected open fun getCurrentTopCornerRadius(): Float { 244 val drawable = background ?: return 0f 245 val gradient = findGradientDrawable(drawable) ?: return 0f 246 247 // TODO(b/184121838): Support more than symmetric top & bottom radius. 248 val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius 249 return radius * ghostedView.scaleX 250 } 251 252 /** Return the current bottom corner radius of the background. */ getCurrentBottomCornerRadiusnull253 protected open fun getCurrentBottomCornerRadius(): Float { 254 val drawable = background ?: return 0f 255 val gradient = findGradientDrawable(drawable) ?: return 0f 256 257 // TODO(b/184121838): Support more than symmetric top & bottom radius. 258 val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius 259 return radius * ghostedView.scaleX 260 } 261 createAnimatorStatenull262 override fun createAnimatorState(): TransitionAnimator.State { 263 val state = 264 TransitionAnimator.State( 265 topCornerRadius = getCurrentTopCornerRadius(), 266 bottomCornerRadius = getCurrentBottomCornerRadius(), 267 ) 268 fillGhostedViewState(state) 269 return state 270 } 271 fillGhostedViewStatenull272 fun fillGhostedViewState(state: TransitionAnimator.State) { 273 // For the animation we are interested in the area that has a non transparent background, 274 // so we have to take the optical insets into account. 275 ghostedView.getLocationOnScreen(ghostedViewLocation) 276 val insets = backgroundInsets 277 val boundCorrections: Rect = 278 if (ghostedView is LaunchableView) { 279 (ghostedView as LaunchableView).getPaddingForLaunchAnimation() 280 } else { 281 Rect() 282 } 283 state.top = ghostedViewLocation[1] + insets.top + boundCorrections.top 284 state.bottom = 285 ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() - 286 insets.bottom + boundCorrections.bottom 287 state.left = ghostedViewLocation[0] + insets.left + boundCorrections.left 288 state.right = 289 ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() - 290 insets.right + boundCorrections.right 291 } 292 onTransitionAnimationStartnull293 override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { 294 if (ghostedView.parent !is ViewGroup) { 295 // This should usually not happen, but let's make sure we don't crash if the view was 296 // detached right before we started the animation. 297 Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup") 298 return 299 } 300 301 backgroundView = 302 FrameLayout(transitionContainer.context).also { transitionContainerOverlay.add(it) } 303 304 // We wrap the ghosted view background and use it to draw the expandable background. Its 305 // alpha will be set to 0 as soon as we start drawing the expanding background. 306 startBackgroundAlpha = background?.alpha ?: 0xFF 307 backgroundDrawable = WrappedDrawable(background) 308 backgroundView?.background = backgroundDrawable 309 310 // Delay the calls to `ghostedView.setVisibility()` during the animation. This must be 311 // called before `GhostView.addGhost()` is called because the latter will change the 312 // *transition* visibility, which won't be blocked and will affect the normal View 313 // visibility that is saved by `setShouldBlockVisibilityChanges()` for a later restoration. 314 (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true) 315 316 try { 317 // Create a ghost of the view that will be moving and fading out. This allows to fade 318 // out the content before fading out the background. 319 ghostView = GhostView.addGhost(ghostedView, transitionContainer) 320 } catch (e: Exception) { 321 // It is not 100% clear what conditions cause this exception to happen in practice, and 322 // we could never reproduce it, but it does show up extremely rarely. We already handle 323 // the scenario where ghostView is null, so we just avoid crashing and log the error. 324 // See b/315858472 for an investigation of the issue. 325 Log.e(TAG, "Failed to create ghostView", e) 326 } 327 328 // [GhostView.addGhost], the result of which is our [ghostView], creates a [GhostView], and 329 // adds it first to a [FrameLayout] container. It then adds _that_ container to an 330 // [OverlayViewGroup]. We need to turn off clipping for that container view. Currently, 331 // however, the only way to get a reference to that overlay is by going through our 332 // [ghostView]. The [OverlayViewGroup] will always be its grandparent view. 333 // TODO(b/306652954) reference the overlay view group directly if we can 334 (ghostView?.parent?.parent as? ViewGroup)?.let { 335 it.clipChildren = false 336 it.clipToPadding = false 337 } 338 339 val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX 340 matrix.getValues(initialGhostViewMatrixValues) 341 342 cujType?.let { interactionJankMonitor.begin(ghostedView, it) } 343 } 344 onTransitionAnimationProgressnull345 override fun onTransitionAnimationProgress( 346 state: TransitionAnimator.State, 347 progress: Float, 348 linearProgress: Float, 349 ) { 350 val ghostView = this.ghostView ?: return 351 val backgroundView = this.backgroundView!! 352 353 if (!state.visible || !ghostedView.isAttachedToWindow) { 354 if (ghostView.visibility == View.VISIBLE) { 355 // Making the ghost view invisible will make the ghosted view visible, so order is 356 // important here. 357 ghostView.visibility = View.INVISIBLE 358 359 // Make the ghosted view invisible again. We use the transition visibility like 360 // GhostView does so that we don't mess up with the accessibility tree (see 361 // b/204944038#comment17). 362 ghostedView.setTransitionVisibility(View.INVISIBLE) 363 backgroundView.visibility = View.INVISIBLE 364 } 365 return 366 } 367 368 // The ghost and backgrounds views were made invisible earlier. That can for instance happen 369 // when animating a dialog into a view. 370 if (ghostView.visibility == View.INVISIBLE) { 371 ghostView.visibility = View.VISIBLE 372 backgroundView.visibility = View.VISIBLE 373 } 374 375 fillGhostedViewState(ghostedViewState) 376 val leftChange = state.left - ghostedViewState.left 377 val rightChange = state.right - ghostedViewState.right 378 val topChange = state.top - ghostedViewState.top 379 val bottomChange = state.bottom - ghostedViewState.bottom 380 381 val widthRatio = state.width.toFloat() / ghostedViewState.width 382 val heightRatio = state.height.toFloat() / ghostedViewState.height 383 val scale = min(widthRatio, heightRatio) 384 385 if (ghostedView.parent is ViewGroup) { 386 // Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted 387 // view is still attached to a ViewGroup, otherwise calculateMatrix will throw. 388 GhostView.calculateMatrix(ghostedView, transitionContainer, ghostViewMatrix) 389 } 390 391 transitionContainer.getLocationOnScreen(transitionContainerLocation) 392 ghostViewMatrix.postScale( 393 scale, 394 scale, 395 ghostedViewState.centerX - transitionContainerLocation[0], 396 ghostedViewState.centerY - transitionContainerLocation[1], 397 ) 398 ghostViewMatrix.postTranslate( 399 (leftChange + rightChange) / 2f, 400 (topChange + bottomChange) / 2f, 401 ) 402 ghostView.animationMatrix = ghostViewMatrix 403 404 // We need to take into account the background insets for the background position. 405 val insets = backgroundInsets 406 val topWithInsets = state.top - insets.top 407 val leftWithInsets = state.left - insets.left 408 val rightWithInsets = state.right + insets.right 409 val bottomWithInsets = state.bottom + insets.bottom 410 411 backgroundView.top = topWithInsets - transitionContainerLocation[1] 412 backgroundView.bottom = bottomWithInsets - transitionContainerLocation[1] 413 backgroundView.left = leftWithInsets - transitionContainerLocation[0] 414 backgroundView.right = rightWithInsets - transitionContainerLocation[0] 415 416 val backgroundDrawable = backgroundDrawable!! 417 backgroundDrawable.wrapped?.let { 418 setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius) 419 } 420 } 421 onTransitionAnimationEndnull422 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { 423 if (ghostView == null) { 424 // We didn't actually run the animation. 425 return 426 } 427 428 cujType?.let { interactionJankMonitor.end(it) } 429 430 backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha 431 432 GhostView.removeGhost(ghostedView) 433 backgroundView?.let { transitionContainerOverlay.remove(it) } 434 435 if (ghostedView is LaunchableView) { 436 // Restore the ghosted view visibility. 437 (ghostedView as LaunchableView).setShouldBlockVisibilityChanges(false) 438 (ghostedView as LaunchableView).onActivityLaunchAnimationEnd() 439 } else { 440 // Make the ghosted view visible. We ensure that the view is considered VISIBLE by 441 // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 442 // for more info). 443 ghostedView.visibility = View.INVISIBLE 444 ghostedView.visibility = View.VISIBLE 445 ghostedView.invalidate() 446 } 447 448 if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) { 449 onDispose() 450 } 451 } 452 453 companion object { 454 private const val CORNER_RADIUS_TOP_INDEX = 0 455 private const val CORNER_RADIUS_BOTTOM_INDEX = 4 456 457 /** 458 * Return the first [GradientDrawable] found in [drawable], or null if none is found. If 459 * [drawable] is a [LayerDrawable], this will return the first layer that has a 460 * [GradientDrawable]. 461 */ findGradientDrawablenull462 fun findGradientDrawable(drawable: Drawable): GradientDrawable? { 463 if (drawable is GradientDrawable) { 464 return drawable 465 } 466 467 if (drawable is InsetDrawable) { 468 return drawable.drawable?.let { findGradientDrawable(it) } 469 } 470 471 if (drawable is LayerDrawable) { 472 for (i in 0 until drawable.numberOfLayers) { 473 val maybeGradient = findGradientDrawable(drawable.getDrawable(i)) 474 if (maybeGradient != null) { 475 return maybeGradient 476 } 477 } 478 } 479 480 if (drawable is StateListDrawable) { 481 return findGradientDrawable(drawable.current) 482 } 483 484 return null 485 } 486 } 487 488 private class WrappedDrawable(val wrapped: Drawable?) : Drawable() { 489 private var currentAlpha = 0xFF 490 private var previousBounds = Rect() 491 <lambda>null492 private var cornerRadii = FloatArray(8) { -1f } 493 private var previousCornerRadii = FloatArray(8) 494 drawnull495 override fun draw(canvas: Canvas) { 496 val wrapped = this.wrapped ?: return 497 498 wrapped.copyBounds(previousBounds) 499 500 wrapped.alpha = currentAlpha 501 wrapped.bounds = bounds 502 applyBackgroundRadii() 503 504 wrapped.draw(canvas) 505 506 // The background view (and therefore this drawable) is drawn before the ghost view, so 507 // the ghosted view background alpha should always be 0 when it is drawn above the 508 // background. 509 wrapped.alpha = 0 510 wrapped.bounds = previousBounds 511 restoreBackgroundRadii() 512 } 513 setAlphanull514 override fun setAlpha(alpha: Int) { 515 if (alpha != currentAlpha) { 516 currentAlpha = alpha 517 invalidateSelf() 518 } 519 } 520 getAlphanull521 override fun getAlpha() = currentAlpha 522 523 override fun getOpacity(): Int { 524 val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT 525 526 val previousAlpha = wrapped.alpha 527 wrapped.alpha = currentAlpha 528 val opacity = wrapped.opacity 529 wrapped.alpha = previousAlpha 530 return opacity 531 } 532 setColorFilternull533 override fun setColorFilter(filter: ColorFilter?) { 534 wrapped?.colorFilter = filter 535 } 536 setBackgroundRadiusnull537 fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) { 538 updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius) 539 invalidateSelf() 540 } 541 updateRadiinull542 private fun updateRadii( 543 radii: FloatArray, 544 topCornerRadius: Float, 545 bottomCornerRadius: Float, 546 ) { 547 radii[0] = topCornerRadius 548 radii[1] = topCornerRadius 549 radii[2] = topCornerRadius 550 radii[3] = topCornerRadius 551 552 radii[4] = bottomCornerRadius 553 radii[5] = bottomCornerRadius 554 radii[6] = bottomCornerRadius 555 radii[7] = bottomCornerRadius 556 } 557 applyBackgroundRadiinull558 private fun applyBackgroundRadii() { 559 if (cornerRadii[0] < 0 || wrapped == null) { 560 return 561 } 562 563 savePreviousBackgroundRadii(wrapped) 564 applyBackgroundRadii(wrapped, cornerRadii) 565 } 566 savePreviousBackgroundRadiinull567 private fun savePreviousBackgroundRadii(background: Drawable) { 568 // TODO(b/184121838): This method assumes that all GradientDrawable in background will 569 // have the same radius. Should we save/restore the radii for each layer instead? 570 val gradient = findGradientDrawable(background) ?: return 571 572 // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we 573 // try to avoid that? 574 val radii = gradient.cornerRadii 575 if (radii != null) { 576 radii.copyInto(previousCornerRadii) 577 } else { 578 // Copy the cornerRadius into previousCornerRadii. 579 val radius = gradient.cornerRadius 580 updateRadii(previousCornerRadii, radius, radius) 581 } 582 } 583 applyBackgroundRadiinull584 private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) { 585 if (drawable is GradientDrawable) { 586 drawable.cornerRadii = radii 587 return 588 } 589 590 if (drawable is InsetDrawable) { 591 drawable.drawable?.let { applyBackgroundRadii(it, radii) } 592 return 593 } 594 595 if (drawable !is LayerDrawable) { 596 return 597 } 598 599 for (i in 0 until drawable.numberOfLayers) { 600 applyBackgroundRadii(drawable.getDrawable(i), radii) 601 } 602 } 603 restoreBackgroundRadiinull604 private fun restoreBackgroundRadii() { 605 if (cornerRadii[0] < 0 || wrapped == null) { 606 return 607 } 608 609 applyBackgroundRadii(wrapped, previousCornerRadii) 610 } 611 } 612 } 613