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