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.graphics.Canvas 20 import android.graphics.ColorFilter 21 import android.graphics.Insets 22 import android.graphics.Matrix 23 import android.graphics.PixelFormat 24 import android.graphics.Rect 25 import android.graphics.drawable.Drawable 26 import android.graphics.drawable.GradientDrawable 27 import android.graphics.drawable.InsetDrawable 28 import android.graphics.drawable.LayerDrawable 29 import android.graphics.drawable.StateListDrawable 30 import android.util.Log 31 import android.view.GhostView 32 import android.view.View 33 import android.view.ViewGroup 34 import android.view.ViewGroupOverlay 35 import android.widget.FrameLayout 36 import com.android.internal.jank.InteractionJankMonitor 37 import java.util.LinkedList 38 import kotlin.math.min 39 import kotlin.math.roundToInt 40 41 private const val TAG = "GhostedViewLaunchAnimatorController" 42 43 /** 44 * A base implementation of [ActivityLaunchAnimator.Controller] which creates a [ghost][GhostView] 45 * of [ghostedView] as well as an expandable background view, which are drawn and animated instead 46 * of the ghosted view. 47 * 48 * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during 49 * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown 50 * during this controller instantiation. 51 * 52 * Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView] 53 * whenever possible instead. 54 */ 55 open class GhostedViewLaunchAnimatorController 56 @JvmOverloads 57 constructor( 58 /** The view that will be ghosted and from which the background will be extracted. */ 59 private val ghostedView: View, 60 61 /** The [InteractionJankMonitor.CujType] associated to this animation. */ 62 private val cujType: Int? = null, 63 private var interactionJankMonitor: InteractionJankMonitor = 64 InteractionJankMonitor.getInstance(), 65 ) : ActivityLaunchAnimator.Controller { 66 67 /** The container to which we will add the ghost view and expanding background. */ 68 override var launchContainer = ghostedView.rootView as ViewGroup 69 private val launchContainerOverlay: ViewGroupOverlay 70 get() = launchContainer.overlay 71 private val launchContainerLocation = IntArray(2) 72 73 /** The ghost view that is drawn and animated instead of the ghosted view. */ 74 private var ghostView: GhostView? = null 75 private val initialGhostViewMatrixValues = FloatArray(9) { 0f } 76 private val ghostViewMatrix = Matrix() 77 78 /** 79 * The expanding background view that will be added to [launchContainer] (below [ghostView]) and 80 * animate. 81 */ 82 private var backgroundView: FrameLayout? = null 83 84 /** 85 * The drawable wrapping the [ghostedView] background and used as background for 86 * [backgroundView]. 87 */ 88 private var backgroundDrawable: WrappedDrawable? = null 89 private val backgroundInsets by lazy { background?.opticalInsets ?: Insets.NONE } 90 private var startBackgroundAlpha: Int = 0xFF 91 92 private val ghostedViewLocation = IntArray(2) 93 private val ghostedViewState = LaunchAnimator.State() 94 95 /** 96 * The background of the [ghostedView]. This background will be used to draw the background of 97 * the background view that is expanding up to the final animation position. 98 * 99 * Note that during the animation, the alpha value value of this background will be set to 0, 100 * then set back to its initial value at the end of the animation. 101 */ 102 private val background: Drawable? 103 104 init { 105 // Make sure the View we launch from implements LaunchableView to avoid visibility issues. 106 if (ghostedView !is LaunchableView) { 107 throw IllegalArgumentException( 108 "A GhostedViewLaunchAnimatorController was created from a View that does not " + 109 "implement LaunchableView. This can lead to subtle bugs where the visibility " + 110 "of the View we are launching from is not what we expected." 111 ) 112 } 113 114 /** Find the first view with a background in [view] and its children. */ 115 fun findBackground(view: View): Drawable? { 116 if (view.background != null) { 117 return view.background 118 } 119 120 // Perform a BFS to find the largest View with background. 121 val views = LinkedList<View>().apply { add(view) } 122 123 while (views.isNotEmpty()) { 124 val v = views.removeFirst() 125 if (v.background != null) { 126 return v.background 127 } 128 129 if (v is ViewGroup) { 130 for (i in 0 until v.childCount) { 131 views.add(v.getChildAt(i)) 132 } 133 } 134 } 135 136 return null 137 } 138 139 background = findBackground(ghostedView) 140 } 141 142 /** 143 * Set the corner radius of [background]. The background is the one that was returned by 144 * [getBackground]. 145 */ 146 protected open fun setBackgroundCornerRadius( 147 background: Drawable, 148 topCornerRadius: Float, 149 bottomCornerRadius: Float 150 ) { 151 // By default, we rely on WrappedDrawable to set/restore the background radii before/after 152 // each draw. 153 backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius) 154 } 155 156 /** Return the current top corner radius of the background. */ 157 protected open fun getCurrentTopCornerRadius(): Float { 158 val drawable = background ?: return 0f 159 val gradient = findGradientDrawable(drawable) ?: return 0f 160 161 // TODO(b/184121838): Support more than symmetric top & bottom radius. 162 val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius 163 return radius * ghostedView.scaleX 164 } 165 166 /** Return the current bottom corner radius of the background. */ 167 protected open fun getCurrentBottomCornerRadius(): Float { 168 val drawable = background ?: return 0f 169 val gradient = findGradientDrawable(drawable) ?: return 0f 170 171 // TODO(b/184121838): Support more than symmetric top & bottom radius. 172 val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius 173 return radius * ghostedView.scaleX 174 } 175 176 override fun createAnimatorState(): LaunchAnimator.State { 177 val state = 178 LaunchAnimator.State( 179 topCornerRadius = getCurrentTopCornerRadius(), 180 bottomCornerRadius = getCurrentBottomCornerRadius() 181 ) 182 fillGhostedViewState(state) 183 return state 184 } 185 186 fun fillGhostedViewState(state: LaunchAnimator.State) { 187 // For the animation we are interested in the area that has a non transparent background, 188 // so we have to take the optical insets into account. 189 ghostedView.getLocationOnScreen(ghostedViewLocation) 190 val insets = backgroundInsets 191 state.top = ghostedViewLocation[1] + insets.top 192 state.bottom = 193 ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() - 194 insets.bottom 195 state.left = ghostedViewLocation[0] + insets.left 196 state.right = 197 ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() - 198 insets.right 199 } 200 201 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { 202 if (ghostedView.parent !is ViewGroup) { 203 // This should usually not happen, but let's make sure we don't crash if the view was 204 // detached right before we started the animation. 205 Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup") 206 return 207 } 208 209 backgroundView = FrameLayout(launchContainer.context).also { 210 launchContainerOverlay.add(it) 211 } 212 213 // We wrap the ghosted view background and use it to draw the expandable background. Its 214 // alpha will be set to 0 as soon as we start drawing the expanding background. 215 startBackgroundAlpha = background?.alpha ?: 0xFF 216 backgroundDrawable = WrappedDrawable(background) 217 backgroundView?.background = backgroundDrawable 218 219 // Delay the calls to `ghostedView.setVisibility()` during the animation. This must be 220 // called before `GhostView.addGhost()` is called because the latter will change the 221 // *transition* visibility, which won't be blocked and will affect the normal View 222 // visibility that is saved by `setShouldBlockVisibilityChanges()` for a later restoration. 223 (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true) 224 225 // Create a ghost of the view that will be moving and fading out. This allows to fade out 226 // the content before fading out the background. 227 ghostView = GhostView.addGhost(ghostedView, launchContainer) 228 229 val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX 230 matrix.getValues(initialGhostViewMatrixValues) 231 232 cujType?.let { interactionJankMonitor.begin(ghostedView, it) } 233 } 234 235 override fun onLaunchAnimationProgress( 236 state: LaunchAnimator.State, 237 progress: Float, 238 linearProgress: Float 239 ) { 240 val ghostView = this.ghostView ?: return 241 val backgroundView = this.backgroundView!! 242 243 if (!state.visible || !ghostedView.isAttachedToWindow) { 244 if (ghostView.visibility == View.VISIBLE) { 245 // Making the ghost view invisible will make the ghosted view visible, so order is 246 // important here. 247 ghostView.visibility = View.INVISIBLE 248 249 // Make the ghosted view invisible again. We use the transition visibility like 250 // GhostView does so that we don't mess up with the accessibility tree (see 251 // b/204944038#comment17). 252 ghostedView.setTransitionVisibility(View.INVISIBLE) 253 backgroundView.visibility = View.INVISIBLE 254 } 255 return 256 } 257 258 // The ghost and backgrounds views were made invisible earlier. That can for instance happen 259 // when animating a dialog into a view. 260 if (ghostView.visibility == View.INVISIBLE) { 261 ghostView.visibility = View.VISIBLE 262 backgroundView.visibility = View.VISIBLE 263 } 264 265 fillGhostedViewState(ghostedViewState) 266 val leftChange = state.left - ghostedViewState.left 267 val rightChange = state.right - ghostedViewState.right 268 val topChange = state.top - ghostedViewState.top 269 val bottomChange = state.bottom - ghostedViewState.bottom 270 271 val widthRatio = state.width.toFloat() / ghostedViewState.width 272 val heightRatio = state.height.toFloat() / ghostedViewState.height 273 val scale = min(widthRatio, heightRatio) 274 275 if (ghostedView.parent is ViewGroup) { 276 // Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted 277 // view is still attached to a ViewGroup, otherwise calculateMatrix will throw. 278 GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix) 279 } 280 281 launchContainer.getLocationOnScreen(launchContainerLocation) 282 ghostViewMatrix.postScale( 283 scale, 284 scale, 285 ghostedViewState.centerX - launchContainerLocation[0], 286 ghostedViewState.centerY - launchContainerLocation[1] 287 ) 288 ghostViewMatrix.postTranslate( 289 (leftChange + rightChange) / 2f, 290 (topChange + bottomChange) / 2f 291 ) 292 ghostView.animationMatrix = ghostViewMatrix 293 294 // We need to take into account the background insets for the background position. 295 val insets = backgroundInsets 296 val topWithInsets = state.top - insets.top 297 val leftWithInsets = state.left - insets.left 298 val rightWithInsets = state.right + insets.right 299 val bottomWithInsets = state.bottom + insets.bottom 300 301 backgroundView.top = topWithInsets - launchContainerLocation[1] 302 backgroundView.bottom = bottomWithInsets - launchContainerLocation[1] 303 backgroundView.left = leftWithInsets - launchContainerLocation[0] 304 backgroundView.right = rightWithInsets - launchContainerLocation[0] 305 306 val backgroundDrawable = backgroundDrawable!! 307 backgroundDrawable.wrapped?.let { 308 setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius) 309 } 310 } 311 312 override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { 313 if (ghostView == null) { 314 // We didn't actually run the animation. 315 return 316 } 317 318 cujType?.let { interactionJankMonitor.end(it) } 319 320 backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha 321 322 GhostView.removeGhost(ghostedView) 323 backgroundView?.let { launchContainerOverlay.remove(it) } 324 325 if (ghostedView is LaunchableView) { 326 // Restore the ghosted view visibility. 327 ghostedView.setShouldBlockVisibilityChanges(false) 328 } else { 329 // Make the ghosted view visible. We ensure that the view is considered VISIBLE by 330 // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 331 // for more info). 332 ghostedView.visibility = View.INVISIBLE 333 ghostedView.visibility = View.VISIBLE 334 ghostedView.invalidate() 335 } 336 } 337 338 companion object { 339 private const val CORNER_RADIUS_TOP_INDEX = 0 340 private const val CORNER_RADIUS_BOTTOM_INDEX = 4 341 342 /** 343 * Return the first [GradientDrawable] found in [drawable], or null if none is found. If 344 * [drawable] is a [LayerDrawable], this will return the first layer that is a 345 * [GradientDrawable]. 346 */ 347 fun findGradientDrawable(drawable: Drawable): GradientDrawable? { 348 if (drawable is GradientDrawable) { 349 return drawable 350 } 351 352 if (drawable is InsetDrawable) { 353 return drawable.drawable?.let { findGradientDrawable(it) } 354 } 355 356 if (drawable is LayerDrawable) { 357 for (i in 0 until drawable.numberOfLayers) { 358 val maybeGradient = drawable.getDrawable(i) 359 if (maybeGradient is GradientDrawable) { 360 return maybeGradient 361 } 362 } 363 } 364 365 if (drawable is StateListDrawable) { 366 return findGradientDrawable(drawable.current) 367 } 368 369 return null 370 } 371 } 372 373 private class WrappedDrawable(val wrapped: Drawable?) : Drawable() { 374 private var currentAlpha = 0xFF 375 private var previousBounds = Rect() 376 377 private var cornerRadii = FloatArray(8) { -1f } 378 private var previousCornerRadii = FloatArray(8) 379 380 override fun draw(canvas: Canvas) { 381 val wrapped = this.wrapped ?: return 382 383 wrapped.copyBounds(previousBounds) 384 385 wrapped.alpha = currentAlpha 386 wrapped.bounds = bounds 387 applyBackgroundRadii() 388 389 wrapped.draw(canvas) 390 391 // The background view (and therefore this drawable) is drawn before the ghost view, so 392 // the ghosted view background alpha should always be 0 when it is drawn above the 393 // background. 394 wrapped.alpha = 0 395 wrapped.bounds = previousBounds 396 restoreBackgroundRadii() 397 } 398 399 override fun setAlpha(alpha: Int) { 400 if (alpha != currentAlpha) { 401 currentAlpha = alpha 402 invalidateSelf() 403 } 404 } 405 406 override fun getAlpha() = currentAlpha 407 408 override fun getOpacity(): Int { 409 val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT 410 411 val previousAlpha = wrapped.alpha 412 wrapped.alpha = currentAlpha 413 val opacity = wrapped.opacity 414 wrapped.alpha = previousAlpha 415 return opacity 416 } 417 418 override fun setColorFilter(filter: ColorFilter?) { 419 wrapped?.colorFilter = filter 420 } 421 422 fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) { 423 updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius) 424 invalidateSelf() 425 } 426 427 private fun updateRadii( 428 radii: FloatArray, 429 topCornerRadius: Float, 430 bottomCornerRadius: Float 431 ) { 432 radii[0] = topCornerRadius 433 radii[1] = topCornerRadius 434 radii[2] = topCornerRadius 435 radii[3] = topCornerRadius 436 437 radii[4] = bottomCornerRadius 438 radii[5] = bottomCornerRadius 439 radii[6] = bottomCornerRadius 440 radii[7] = bottomCornerRadius 441 } 442 443 private fun applyBackgroundRadii() { 444 if (cornerRadii[0] < 0 || wrapped == null) { 445 return 446 } 447 448 savePreviousBackgroundRadii(wrapped) 449 applyBackgroundRadii(wrapped, cornerRadii) 450 } 451 452 private fun savePreviousBackgroundRadii(background: Drawable) { 453 // TODO(b/184121838): This method assumes that all GradientDrawable in background will 454 // have the same radius. Should we save/restore the radii for each layer instead? 455 val gradient = findGradientDrawable(background) ?: return 456 457 // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we 458 // try to avoid that? 459 val radii = gradient.cornerRadii 460 if (radii != null) { 461 radii.copyInto(previousCornerRadii) 462 } else { 463 // Copy the cornerRadius into previousCornerRadii. 464 val radius = gradient.cornerRadius 465 updateRadii(previousCornerRadii, radius, radius) 466 } 467 } 468 469 private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) { 470 if (drawable is GradientDrawable) { 471 drawable.cornerRadii = radii 472 return 473 } 474 475 if (drawable is InsetDrawable) { 476 drawable.drawable?.let { applyBackgroundRadii(it, radii) } 477 return 478 } 479 480 if (drawable !is LayerDrawable) { 481 return 482 } 483 484 for (i in 0 until drawable.numberOfLayers) { 485 (drawable.getDrawable(i) as? GradientDrawable)?.cornerRadii = radii 486 } 487 } 488 489 private fun restoreBackgroundRadii() { 490 if (cornerRadii[0] < 0 || wrapped == null) { 491 return 492 } 493 494 applyBackgroundRadii(wrapped, previousCornerRadii) 495 } 496 } 497 } 498