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.lang.IllegalArgumentException 38 import java.util.LinkedList 39 import kotlin.math.min 40 import kotlin.math.roundToInt 41 42 private const val TAG = "GhostedViewLaunchAnimatorController" 43 44 /** 45 * A base implementation of [ActivityLaunchAnimator.Controller] which creates a [ghost][GhostView] 46 * of [ghostedView] as well as an expandable background view, which are drawn and animated instead 47 * of the ghosted view. 48 * 49 * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during 50 * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown 51 * during this controller instantiation. 52 * 53 * Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView] 54 * whenever possible instead. 55 */ 56 open class GhostedViewLaunchAnimatorController 57 @JvmOverloads 58 constructor( 59 /** The view that will be ghosted and from which the background will be extracted. */ 60 private val ghostedView: View, 61 62 /** The [InteractionJankMonitor.CujType] associated to this animation. */ 63 private val cujType: Int? = null, 64 private var interactionJankMonitor: InteractionJankMonitor = 65 InteractionJankMonitor.getInstance(), 66 ) : ActivityLaunchAnimator.Controller { 67 68 /** The container to which we will add the ghost view and expanding background. */ 69 override var launchContainer = ghostedView.rootView as ViewGroup 70 private val launchContainerOverlay: ViewGroupOverlay 71 get() = launchContainer.overlay 72 private val launchContainerLocation = IntArray(2) 73 74 /** The ghost view that is drawn and animated instead of the ghosted view. */ 75 private var ghostView: GhostView? = null <lambda>null76 private val initialGhostViewMatrixValues = FloatArray(9) { 0f } 77 private val ghostViewMatrix = Matrix() 78 79 /** 80 * The expanding background view that will be added to [launchContainer] (below [ghostView]) and 81 * animate. 82 */ 83 private var backgroundView: FrameLayout? = null 84 85 /** 86 * The drawable wrapping the [ghostedView] background and used as background for 87 * [backgroundView]. 88 */ 89 private var backgroundDrawable: WrappedDrawable? = null <lambda>null90 private val backgroundInsets by lazy { background?.opticalInsets ?: Insets.NONE } 91 private var startBackgroundAlpha: Int = 0xFF 92 93 private val ghostedViewLocation = IntArray(2) 94 private val ghostedViewState = LaunchAnimator.State() 95 96 /** 97 * The background of the [ghostedView]. This background will be used to draw the background of 98 * the background view that is expanding up to the final animation position. 99 * 100 * Note that during the animation, the alpha value value of this background will be set to 0, 101 * then set back to its initial value at the end of the animation. 102 */ 103 private val background: Drawable? 104 105 init { 106 // Make sure the View we launch from implements LaunchableView to avoid visibility issues. 107 if (ghostedView !is LaunchableView) { 108 throw IllegalArgumentException( 109 "A GhostedViewLaunchAnimatorController was created from a View that does not " + 110 "implement LaunchableView. This can lead to subtle bugs where the visibility " + 111 "of the View we are launching from is not what we expected." 112 ) 113 } 114 115 /** Find the first view with a background in [view] and its children. */ findBackgroundnull116 fun findBackground(view: View): Drawable? { 117 if (view.background != null) { 118 return view.background 119 } 120 121 // Perform a BFS to find the largest View with background. 122 val views = LinkedList<View>().apply { add(view) } 123 124 while (views.isNotEmpty()) { 125 val v = views.removeFirst() 126 if (v.background != null) { 127 return v.background 128 } 129 130 if (v is ViewGroup) { 131 for (i in 0 until v.childCount) { 132 views.add(v.getChildAt(i)) 133 } 134 } 135 } 136 137 return null 138 } 139 140 background = findBackground(ghostedView) 141 } 142 143 /** 144 * Set the corner radius of [background]. The background is the one that was returned by 145 * [getBackground]. 146 */ setBackgroundCornerRadiusnull147 protected open fun setBackgroundCornerRadius( 148 background: Drawable, 149 topCornerRadius: Float, 150 bottomCornerRadius: Float 151 ) { 152 // By default, we rely on WrappedDrawable to set/restore the background radii before/after 153 // each draw. 154 backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius) 155 } 156 157 /** Return the current top corner radius of the background. */ getCurrentTopCornerRadiusnull158 protected open fun getCurrentTopCornerRadius(): Float { 159 val drawable = background ?: return 0f 160 val gradient = findGradientDrawable(drawable) ?: return 0f 161 162 // TODO(b/184121838): Support more than symmetric top & bottom radius. 163 val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius 164 return radius * ghostedView.scaleX 165 } 166 167 /** Return the current bottom corner radius of the background. */ getCurrentBottomCornerRadiusnull168 protected open fun getCurrentBottomCornerRadius(): Float { 169 val drawable = background ?: return 0f 170 val gradient = findGradientDrawable(drawable) ?: return 0f 171 172 // TODO(b/184121838): Support more than symmetric top & bottom radius. 173 val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius 174 return radius * ghostedView.scaleX 175 } 176 createAnimatorStatenull177 override fun createAnimatorState(): LaunchAnimator.State { 178 val state = 179 LaunchAnimator.State( 180 topCornerRadius = getCurrentTopCornerRadius(), 181 bottomCornerRadius = getCurrentBottomCornerRadius() 182 ) 183 fillGhostedViewState(state) 184 return state 185 } 186 fillGhostedViewStatenull187 fun fillGhostedViewState(state: LaunchAnimator.State) { 188 // For the animation we are interested in the area that has a non transparent background, 189 // so we have to take the optical insets into account. 190 ghostedView.getLocationOnScreen(ghostedViewLocation) 191 val insets = backgroundInsets 192 state.top = ghostedViewLocation[1] + insets.top 193 state.bottom = 194 ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() - 195 insets.bottom 196 state.left = ghostedViewLocation[0] + insets.left 197 state.right = 198 ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() - 199 insets.right 200 } 201 onLaunchAnimationStartnull202 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { 203 if (ghostedView.parent !is ViewGroup) { 204 // This should usually not happen, but let's make sure we don't crash if the view was 205 // detached right before we started the animation. 206 Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup") 207 return 208 } 209 210 backgroundView = FrameLayout(launchContainer.context) 211 launchContainerOverlay.add(backgroundView) 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 onLaunchAnimationProgressnull235 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) { 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 onLaunchAnimationEndnull312 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 launchContainerOverlay.remove(backgroundView) 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 */ findGradientDrawablenull347 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 <lambda>null377 private var cornerRadii = FloatArray(8) { -1f } 378 private var previousCornerRadii = FloatArray(8) 379 drawnull380 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 setAlphanull399 override fun setAlpha(alpha: Int) { 400 if (alpha != currentAlpha) { 401 currentAlpha = alpha 402 invalidateSelf() 403 } 404 } 405 getAlphanull406 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 setColorFilternull418 override fun setColorFilter(filter: ColorFilter?) { 419 wrapped?.colorFilter = filter 420 } 421 setBackgroundRadiusnull422 fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) { 423 updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius) 424 invalidateSelf() 425 } 426 updateRadiinull427 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 applyBackgroundRadiinull443 private fun applyBackgroundRadii() { 444 if (cornerRadii[0] < 0 || wrapped == null) { 445 return 446 } 447 448 savePreviousBackgroundRadii(wrapped) 449 applyBackgroundRadii(wrapped, cornerRadii) 450 } 451 savePreviousBackgroundRadiinull452 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 applyBackgroundRadiinull469 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 restoreBackgroundRadiinull489 private fun restoreBackgroundRadii() { 490 if (cornerRadii[0] < 0 || wrapped == null) { 491 return 492 } 493 494 applyBackgroundRadii(wrapped, previousCornerRadii) 495 } 496 } 497 } 498