1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.animation 18 19 import android.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 java.util.LinkedList 40 import kotlin.math.min 41 import kotlin.math.roundToInt 42 43 private const val TAG = "GhostedViewTransitionAnimatorController" 44 45 /** 46 * A base implementation of [ActivityTransitionAnimator.Controller] which creates a 47 * [ghost][GhostView] of [ghostedView] as well as an expandable background view, which are drawn and 48 * animated instead of the ghosted view. 49 * 50 * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during 51 * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown 52 * during this controller instantiation. 53 * 54 * Note: Avoid instantiating this directly and call [ActivityTransitionAnimator.Controller.fromView] 55 * whenever possible instead. 56 */ 57 open class GhostedViewTransitionAnimatorController 58 @JvmOverloads 59 constructor( 60 /** The view that will be ghosted and from which the background will be extracted. */ 61 private val ghostedView: View, 62 63 /** The [CujType] associated to this launch animation. */ 64 private val launchCujType: Int? = null, 65 override val transitionCookie: ActivityTransitionAnimator.TransitionCookie? = null, 66 override val component: ComponentName? = null, 67 68 /** The [CujType] associated to this return animation. */ 69 private val returnCujType: Int? = null, 70 71 /** 72 * Whether this controller should be invalidated after its first use, and whenever [ghostedView] 73 * is detached. 74 */ 75 private val isEphemeral: Boolean = false, 76 private var interactionJankMonitor: InteractionJankMonitor = 77 InteractionJankMonitor.getInstance(), 78 ) : ActivityTransitionAnimator.Controller { 79 override val isLaunching: Boolean = true 80 81 /** The container to which we will add the ghost view and expanding background. */ 82 override var transitionContainer = ghostedView.rootView as ViewGroup 83 private val transitionContainerOverlay: ViewGroupOverlay 84 get() = transitionContainer.overlay 85 86 private val transitionContainerLocation = IntArray(2) 87 88 /** The ghost view that is drawn and animated instead of the ghosted view. */ 89 private var ghostView: GhostView? = null <lambda>null90 private val initialGhostViewMatrixValues = FloatArray(9) { 0f } 91 private val ghostViewMatrix = Matrix() 92 93 /** 94 * The expanding background view that will be added to [transitionContainer] (below [ghostView]) 95 * and animate. 96 */ 97 private var backgroundView: FrameLayout? = null 98 99 /** 100 * The drawable wrapping the [ghostedView] background and used as background for 101 * [backgroundView]. 102 */ 103 private var backgroundDrawable: WrappedDrawable? = null <lambda>null104 private val backgroundInsets by lazy { background?.opticalInsets ?: Insets.NONE } 105 private var startBackgroundAlpha: Int = 0xFF 106 107 private val ghostedViewLocation = IntArray(2) 108 private val ghostedViewState = TransitionAnimator.State() 109 110 /** 111 * The background of the [ghostedView]. This background will be used to draw the background of 112 * the background view that is expanding up to the final animation position. 113 * 114 * Note that during the animation, the alpha value value of this background will be set to 0, 115 * then set back to its initial value at the end of the animation. 116 */ 117 private val background: Drawable? 118 119 /** CUJ identifier accounting for whether this controller is for a launch or a return. */ 120 private val cujType: Int? 121 get() = 122 if (isLaunching) { 123 launchCujType 124 } else { 125 returnCujType 126 } 127 128 /** 129 * Used to automatically clean up the internal state once [ghostedView] is detached from the 130 * hierarchy. 131 */ 132 private val detachListener = 133 object : View.OnAttachStateChangeListener { onViewAttachedToWindownull134 override fun onViewAttachedToWindow(v: View) {} 135 onViewDetachedFromWindownull136 override fun onViewDetachedFromWindow(v: View) { 137 onDispose() 138 } 139 } 140 141 init { 142 // Make sure the View we launch from implements LaunchableView to avoid visibility issues. 143 if (ghostedView !is LaunchableView) { 144 throw IllegalArgumentException( 145 "A GhostedViewLaunchAnimatorController was created from a View that does not " + 146 "implement LaunchableView. This can lead to subtle bugs where the visibility " + 147 "of the View we are launching from is not what we expected." 148 ) 149 } 150 151 /** Find the first view with a background in [view] and its children. */ findBackgroundnull152 fun findBackground(view: View): Drawable? { 153 if (view.background != null) { 154 return view.background 155 } 156 157 // Perform a BFS to find the largest View with background. 158 val views = LinkedList<View>().apply { add(view) } 159 160 while (views.isNotEmpty()) { 161 val v = views.removeAt(0) 162 if (v.background != null) { 163 return v.background 164 } 165 166 if (v is ViewGroup) { 167 for (i in 0 until v.childCount) { 168 views.add(v.getChildAt(i)) 169 } 170 } 171 } 172 173 return null 174 } 175 176 background = findBackground(ghostedView) 177 178 if (TransitionAnimator.returnAnimationsEnabled() && isEphemeral) { 179 ghostedView.addOnAttachStateChangeListener(detachListener) 180 } 181 } 182 onDisposenull183 override fun onDispose() { 184 if (TransitionAnimator.returnAnimationsEnabled()) { 185 ghostedView.removeOnAttachStateChangeListener(detachListener) 186 } 187 } 188 189 /** 190 * Set the corner radius of [background]. The background is the one that was returned by 191 * [getBackground]. 192 */ setBackgroundCornerRadiusnull193 protected open fun setBackgroundCornerRadius( 194 background: Drawable, 195 topCornerRadius: Float, 196 bottomCornerRadius: Float, 197 ) { 198 // By default, we rely on WrappedDrawable to set/restore the background radii before/after 199 // each draw. 200 backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius) 201 } 202 203 /** Return the current top corner radius of the background. */ getCurrentTopCornerRadiusnull204 protected open fun getCurrentTopCornerRadius(): Float { 205 val drawable = background ?: return 0f 206 val gradient = findGradientDrawable(drawable) ?: return 0f 207 208 // TODO(b/184121838): Support more than symmetric top & bottom radius. 209 val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius 210 return radius * ghostedView.scaleX 211 } 212 213 /** Return the current bottom corner radius of the background. */ getCurrentBottomCornerRadiusnull214 protected open fun getCurrentBottomCornerRadius(): Float { 215 val drawable = background ?: return 0f 216 val gradient = findGradientDrawable(drawable) ?: return 0f 217 218 // TODO(b/184121838): Support more than symmetric top & bottom radius. 219 val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius 220 return radius * ghostedView.scaleX 221 } 222 createAnimatorStatenull223 override fun createAnimatorState(): TransitionAnimator.State { 224 val state = 225 TransitionAnimator.State( 226 topCornerRadius = getCurrentTopCornerRadius(), 227 bottomCornerRadius = getCurrentBottomCornerRadius(), 228 ) 229 fillGhostedViewState(state) 230 return state 231 } 232 fillGhostedViewStatenull233 fun fillGhostedViewState(state: TransitionAnimator.State) { 234 // For the animation we are interested in the area that has a non transparent background, 235 // so we have to take the optical insets into account. 236 ghostedView.getLocationOnScreen(ghostedViewLocation) 237 val insets = backgroundInsets 238 val boundCorrections: Rect = 239 if (ghostedView is LaunchableView) { 240 ghostedView.getPaddingForLaunchAnimation() 241 } else { 242 Rect() 243 } 244 state.top = ghostedViewLocation[1] + insets.top + boundCorrections.top 245 state.bottom = 246 ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() - 247 insets.bottom + boundCorrections.bottom 248 state.left = ghostedViewLocation[0] + insets.left + boundCorrections.left 249 state.right = 250 ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() - 251 insets.right + boundCorrections.right 252 } 253 onTransitionAnimationStartnull254 override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { 255 if (ghostedView.parent !is ViewGroup) { 256 // This should usually not happen, but let's make sure we don't crash if the view was 257 // detached right before we started the animation. 258 Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup") 259 return 260 } 261 262 backgroundView = 263 FrameLayout(transitionContainer.context).also { transitionContainerOverlay.add(it) } 264 265 // We wrap the ghosted view background and use it to draw the expandable background. Its 266 // alpha will be set to 0 as soon as we start drawing the expanding background. 267 startBackgroundAlpha = background?.alpha ?: 0xFF 268 backgroundDrawable = WrappedDrawable(background) 269 backgroundView?.background = backgroundDrawable 270 271 // Delay the calls to `ghostedView.setVisibility()` during the animation. This must be 272 // called before `GhostView.addGhost()` is called because the latter will change the 273 // *transition* visibility, which won't be blocked and will affect the normal View 274 // visibility that is saved by `setShouldBlockVisibilityChanges()` for a later restoration. 275 (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true) 276 277 // Create a ghost of the view that will be moving and fading out. This allows to fade out 278 // the content before fading out the background. 279 ghostView = GhostView.addGhost(ghostedView, transitionContainer) 280 281 // [GhostView.addGhost], the result of which is our [ghostView], creates a [GhostView], and 282 // adds it first to a [FrameLayout] container. It then adds _that_ container to an 283 // [OverlayViewGroup]. We need to turn off clipping for that container view. Currently, 284 // however, the only way to get a reference to that overlay is by going through our 285 // [ghostView]. The [OverlayViewGroup] will always be its grandparent view. 286 // TODO(b/306652954) reference the overlay view group directly if we can 287 (ghostView?.parent?.parent as? ViewGroup)?.let { 288 it.clipChildren = false 289 it.clipToPadding = false 290 } 291 292 val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX 293 matrix.getValues(initialGhostViewMatrixValues) 294 295 cujType?.let { interactionJankMonitor.begin(ghostedView, it) } 296 } 297 onTransitionAnimationProgressnull298 override fun onTransitionAnimationProgress( 299 state: TransitionAnimator.State, 300 progress: Float, 301 linearProgress: Float, 302 ) { 303 val ghostView = this.ghostView ?: return 304 val backgroundView = this.backgroundView!! 305 306 if (!state.visible || !ghostedView.isAttachedToWindow) { 307 if (ghostView.visibility == View.VISIBLE) { 308 // Making the ghost view invisible will make the ghosted view visible, so order is 309 // important here. 310 ghostView.visibility = View.INVISIBLE 311 312 // Make the ghosted view invisible again. We use the transition visibility like 313 // GhostView does so that we don't mess up with the accessibility tree (see 314 // b/204944038#comment17). 315 ghostedView.setTransitionVisibility(View.INVISIBLE) 316 backgroundView.visibility = View.INVISIBLE 317 } 318 return 319 } 320 321 // The ghost and backgrounds views were made invisible earlier. That can for instance happen 322 // when animating a dialog into a view. 323 if (ghostView.visibility == View.INVISIBLE) { 324 ghostView.visibility = View.VISIBLE 325 backgroundView.visibility = View.VISIBLE 326 } 327 328 fillGhostedViewState(ghostedViewState) 329 val leftChange = state.left - ghostedViewState.left 330 val rightChange = state.right - ghostedViewState.right 331 val topChange = state.top - ghostedViewState.top 332 val bottomChange = state.bottom - ghostedViewState.bottom 333 334 val widthRatio = state.width.toFloat() / ghostedViewState.width 335 val heightRatio = state.height.toFloat() / ghostedViewState.height 336 val scale = min(widthRatio, heightRatio) 337 338 if (ghostedView.parent is ViewGroup) { 339 // Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted 340 // view is still attached to a ViewGroup, otherwise calculateMatrix will throw. 341 GhostView.calculateMatrix(ghostedView, transitionContainer, ghostViewMatrix) 342 } 343 344 transitionContainer.getLocationOnScreen(transitionContainerLocation) 345 ghostViewMatrix.postScale( 346 scale, 347 scale, 348 ghostedViewState.centerX - transitionContainerLocation[0], 349 ghostedViewState.centerY - transitionContainerLocation[1], 350 ) 351 ghostViewMatrix.postTranslate( 352 (leftChange + rightChange) / 2f, 353 (topChange + bottomChange) / 2f, 354 ) 355 ghostView.animationMatrix = ghostViewMatrix 356 357 // We need to take into account the background insets for the background position. 358 val insets = backgroundInsets 359 val topWithInsets = state.top - insets.top 360 val leftWithInsets = state.left - insets.left 361 val rightWithInsets = state.right + insets.right 362 val bottomWithInsets = state.bottom + insets.bottom 363 364 backgroundView.top = topWithInsets - transitionContainerLocation[1] 365 backgroundView.bottom = bottomWithInsets - transitionContainerLocation[1] 366 backgroundView.left = leftWithInsets - transitionContainerLocation[0] 367 backgroundView.right = rightWithInsets - transitionContainerLocation[0] 368 369 val backgroundDrawable = backgroundDrawable!! 370 backgroundDrawable.wrapped?.let { 371 setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius) 372 } 373 } 374 onTransitionAnimationEndnull375 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { 376 if (ghostView == null) { 377 // We didn't actually run the animation. 378 return 379 } 380 381 cujType?.let { interactionJankMonitor.end(it) } 382 383 backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha 384 385 GhostView.removeGhost(ghostedView) 386 backgroundView?.let { transitionContainerOverlay.remove(it) } 387 388 if (ghostedView is LaunchableView) { 389 // Restore the ghosted view visibility. 390 ghostedView.setShouldBlockVisibilityChanges(false) 391 ghostedView.onActivityLaunchAnimationEnd() 392 } else { 393 // Make the ghosted view visible. We ensure that the view is considered VISIBLE by 394 // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 395 // for more info). 396 ghostedView.visibility = View.INVISIBLE 397 ghostedView.visibility = View.VISIBLE 398 ghostedView.invalidate() 399 } 400 401 if (isEphemeral) { 402 onDispose() 403 } 404 } 405 406 companion object { 407 private const val CORNER_RADIUS_TOP_INDEX = 0 408 private const val CORNER_RADIUS_BOTTOM_INDEX = 4 409 410 /** 411 * Return the first [GradientDrawable] found in [drawable], or null if none is found. If 412 * [drawable] is a [LayerDrawable], this will return the first layer that has a 413 * [GradientDrawable]. 414 */ findGradientDrawablenull415 fun findGradientDrawable(drawable: Drawable): GradientDrawable? { 416 if (drawable is GradientDrawable) { 417 return drawable 418 } 419 420 if (drawable is InsetDrawable) { 421 return drawable.drawable?.let { findGradientDrawable(it) } 422 } 423 424 if (drawable is LayerDrawable) { 425 for (i in 0 until drawable.numberOfLayers) { 426 val maybeGradient = findGradientDrawable(drawable.getDrawable(i)) 427 if (maybeGradient != null) { 428 return maybeGradient 429 } 430 } 431 } 432 433 if (drawable is StateListDrawable) { 434 return findGradientDrawable(drawable.current) 435 } 436 437 return null 438 } 439 } 440 441 private class WrappedDrawable(val wrapped: Drawable?) : Drawable() { 442 private var currentAlpha = 0xFF 443 private var previousBounds = Rect() 444 <lambda>null445 private var cornerRadii = FloatArray(8) { -1f } 446 private var previousCornerRadii = FloatArray(8) 447 drawnull448 override fun draw(canvas: Canvas) { 449 val wrapped = this.wrapped ?: return 450 451 wrapped.copyBounds(previousBounds) 452 453 wrapped.alpha = currentAlpha 454 wrapped.bounds = bounds 455 applyBackgroundRadii() 456 457 wrapped.draw(canvas) 458 459 // The background view (and therefore this drawable) is drawn before the ghost view, so 460 // the ghosted view background alpha should always be 0 when it is drawn above the 461 // background. 462 wrapped.alpha = 0 463 wrapped.bounds = previousBounds 464 restoreBackgroundRadii() 465 } 466 setAlphanull467 override fun setAlpha(alpha: Int) { 468 if (alpha != currentAlpha) { 469 currentAlpha = alpha 470 invalidateSelf() 471 } 472 } 473 getAlphanull474 override fun getAlpha() = currentAlpha 475 476 override fun getOpacity(): Int { 477 val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT 478 479 val previousAlpha = wrapped.alpha 480 wrapped.alpha = currentAlpha 481 val opacity = wrapped.opacity 482 wrapped.alpha = previousAlpha 483 return opacity 484 } 485 setColorFilternull486 override fun setColorFilter(filter: ColorFilter?) { 487 wrapped?.colorFilter = filter 488 } 489 setBackgroundRadiusnull490 fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) { 491 updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius) 492 invalidateSelf() 493 } 494 updateRadiinull495 private fun updateRadii( 496 radii: FloatArray, 497 topCornerRadius: Float, 498 bottomCornerRadius: Float, 499 ) { 500 radii[0] = topCornerRadius 501 radii[1] = topCornerRadius 502 radii[2] = topCornerRadius 503 radii[3] = topCornerRadius 504 505 radii[4] = bottomCornerRadius 506 radii[5] = bottomCornerRadius 507 radii[6] = bottomCornerRadius 508 radii[7] = bottomCornerRadius 509 } 510 applyBackgroundRadiinull511 private fun applyBackgroundRadii() { 512 if (cornerRadii[0] < 0 || wrapped == null) { 513 return 514 } 515 516 savePreviousBackgroundRadii(wrapped) 517 applyBackgroundRadii(wrapped, cornerRadii) 518 } 519 savePreviousBackgroundRadiinull520 private fun savePreviousBackgroundRadii(background: Drawable) { 521 // TODO(b/184121838): This method assumes that all GradientDrawable in background will 522 // have the same radius. Should we save/restore the radii for each layer instead? 523 val gradient = findGradientDrawable(background) ?: return 524 525 // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we 526 // try to avoid that? 527 val radii = gradient.cornerRadii 528 if (radii != null) { 529 radii.copyInto(previousCornerRadii) 530 } else { 531 // Copy the cornerRadius into previousCornerRadii. 532 val radius = gradient.cornerRadius 533 updateRadii(previousCornerRadii, radius, radius) 534 } 535 } 536 applyBackgroundRadiinull537 private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) { 538 if (drawable is GradientDrawable) { 539 drawable.cornerRadii = radii 540 return 541 } 542 543 if (drawable is InsetDrawable) { 544 drawable.drawable?.let { applyBackgroundRadii(it, radii) } 545 return 546 } 547 548 if (drawable !is LayerDrawable) { 549 return 550 } 551 552 for (i in 0 until drawable.numberOfLayers) { 553 applyBackgroundRadii(drawable.getDrawable(i), radii) 554 } 555 } 556 restoreBackgroundRadiinull557 private fun restoreBackgroundRadii() { 558 if (cornerRadii[0] < 0 || wrapped == null) { 559 return 560 } 561 562 applyBackgroundRadii(wrapped, previousCornerRadii) 563 } 564 } 565 } 566