1 package com.android.systemui.animation 2 3 import android.graphics.Canvas 4 import android.graphics.ColorFilter 5 import android.graphics.Matrix 6 import android.graphics.PixelFormat 7 import android.graphics.Rect 8 import android.graphics.drawable.Drawable 9 import android.graphics.drawable.GradientDrawable 10 import android.graphics.drawable.InsetDrawable 11 import android.graphics.drawable.LayerDrawable 12 import android.util.Log 13 import android.view.GhostView 14 import android.view.View 15 import android.view.ViewGroup 16 import android.view.ViewGroupOverlay 17 import android.widget.FrameLayout 18 import com.android.internal.jank.InteractionJankMonitor 19 import kotlin.math.min 20 21 private const val TAG = "GhostedViewLaunchAnimatorController" 22 23 /** 24 * A base implementation of [ActivityLaunchAnimator.Controller] which creates a [ghost][GhostView] 25 * of [ghostedView] as well as an expandable background view, which are drawn and animated instead 26 * of the ghosted view. 27 * 28 * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during 29 * the animation. 30 * 31 * Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView] 32 * whenever possible instead. 33 */ 34 open class GhostedViewLaunchAnimatorController( 35 /** The view that will be ghosted and from which the background will be extracted. */ 36 private val ghostedView: View, 37 38 /** The [InteractionJankMonitor.CujType] associated to this animation. */ 39 private val cujType: Int? = null 40 ) : ActivityLaunchAnimator.Controller { 41 /** The container to which we will add the ghost view and expanding background. */ 42 override var launchContainer = ghostedView.rootView as ViewGroup 43 private val launchContainerOverlay: ViewGroupOverlay 44 get() = launchContainer.overlay 45 46 /** The ghost view that is drawn and animated instead of the ghosted view. */ 47 private var ghostView: GhostView? = null <lambda>null48 private val initialGhostViewMatrixValues = FloatArray(9) { 0f } 49 private val ghostViewMatrix = Matrix() 50 51 /** 52 * The expanding background view that will be added to [launchContainer] (below [ghostView]) and 53 * animate. 54 */ 55 private var backgroundView: FrameLayout? = null 56 57 /** 58 * The drawable wrapping the [ghostedView] background and used as background for 59 * [backgroundView]. 60 */ 61 private var backgroundDrawable: WrappedDrawable? = null 62 private var startBackgroundAlpha: Int = 0xFF 63 64 /** 65 * Return the background of the [ghostedView]. This background will be used to draw the 66 * background of the background view that is expanding up to the final animation position. This 67 * is called at the start of the animation. 68 * 69 * Note that during the animation, the alpha value value of this background will be set to 0, 70 * then set back to its initial value at the end of the animation. 71 */ getBackgroundnull72 protected open fun getBackground(): Drawable? = ghostedView.background 73 74 /** 75 * Set the corner radius of [background]. The background is the one that was returned by 76 * [getBackground]. 77 */ 78 protected open fun setBackgroundCornerRadius( 79 background: Drawable, 80 topCornerRadius: Float, 81 bottomCornerRadius: Float 82 ) { 83 // By default, we rely on WrappedDrawable to set/restore the background radii before/after 84 // each draw. 85 backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius) 86 } 87 88 /** Return the current top corner radius of the background. */ getCurrentTopCornerRadiusnull89 protected open fun getCurrentTopCornerRadius(): Float { 90 val drawable = getBackground() ?: return 0f 91 val gradient = findGradientDrawable(drawable) ?: return 0f 92 93 // TODO(b/184121838): Support more than symmetric top & bottom radius. 94 return gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius 95 } 96 97 /** Return the current bottom corner radius of the background. */ getCurrentBottomCornerRadiusnull98 protected open fun getCurrentBottomCornerRadius(): Float { 99 val drawable = getBackground() ?: return 0f 100 val gradient = findGradientDrawable(drawable) ?: return 0f 101 102 // TODO(b/184121838): Support more than symmetric top & bottom radius. 103 return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius 104 } 105 createAnimatorStatenull106 override fun createAnimatorState(): ActivityLaunchAnimator.State { 107 val location = ghostedView.locationOnScreen 108 return ActivityLaunchAnimator.State( 109 top = location[1], 110 bottom = location[1] + ghostedView.height, 111 left = location[0], 112 right = location[0] + ghostedView.width, 113 topCornerRadius = getCurrentTopCornerRadius(), 114 bottomCornerRadius = getCurrentBottomCornerRadius() 115 ) 116 } 117 onLaunchAnimationStartnull118 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { 119 if (ghostedView.parent !is ViewGroup) { 120 // This should usually not happen, but let's make sure we don't crash if the view was 121 // detached right before we started the animation. 122 Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup") 123 return 124 } 125 126 backgroundView = FrameLayout(launchContainer.context) 127 launchContainerOverlay.add(backgroundView) 128 129 // We wrap the ghosted view background and use it to draw the expandable background. Its 130 // alpha will be set to 0 as soon as we start drawing the expanding background. 131 val drawable = getBackground() 132 startBackgroundAlpha = drawable?.alpha ?: 0xFF 133 backgroundDrawable = WrappedDrawable(drawable) 134 backgroundView?.background = backgroundDrawable 135 136 // Create a ghost of the view that will be moving and fading out. This allows to fade out 137 // the content before fading out the background. 138 ghostView = GhostView.addGhost(ghostedView, launchContainer) 139 140 val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX 141 matrix.getValues(initialGhostViewMatrixValues) 142 143 cujType?.let { InteractionJankMonitor.getInstance().begin(ghostedView, it) } 144 } 145 onLaunchAnimationProgressnull146 override fun onLaunchAnimationProgress( 147 state: ActivityLaunchAnimator.State, 148 progress: Float, 149 linearProgress: Float 150 ) { 151 val ghostView = this.ghostView ?: return 152 val backgroundView = this.backgroundView!! 153 154 if (!state.visible) { 155 if (ghostView.visibility == View.VISIBLE) { 156 // Making the ghost view invisible will make the ghosted view visible, so order is 157 // important here. 158 ghostView.visibility = View.INVISIBLE 159 ghostedView.visibility = View.INVISIBLE 160 backgroundView.visibility = View.INVISIBLE 161 } 162 return 163 } 164 165 val scale = min(state.widthRatio, state.heightRatio) 166 ghostViewMatrix.setValues(initialGhostViewMatrixValues) 167 ghostViewMatrix.postScale(scale, scale, state.startCenterX, state.startCenterY) 168 ghostViewMatrix.postTranslate( 169 (state.leftChange + state.rightChange) / 2f, 170 (state.topChange + state.bottomChange) / 2f 171 ) 172 ghostView.animationMatrix = ghostViewMatrix 173 174 backgroundView.top = state.top 175 backgroundView.bottom = state.bottom 176 backgroundView.left = state.left 177 backgroundView.right = state.right 178 179 val backgroundDrawable = backgroundDrawable!! 180 backgroundDrawable.wrapped?.let { 181 setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius) 182 } 183 } 184 onLaunchAnimationEndnull185 override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { 186 if (ghostView == null) { 187 // We didn't actually run the animation. 188 return 189 } 190 191 cujType?.let { InteractionJankMonitor.getInstance().end(it) } 192 193 backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha 194 195 GhostView.removeGhost(ghostedView) 196 launchContainerOverlay.remove(backgroundView) 197 ghostedView.visibility = View.VISIBLE 198 ghostedView.invalidate() 199 } 200 201 companion object { 202 private const val CORNER_RADIUS_TOP_INDEX = 0 203 private const val CORNER_RADIUS_BOTTOM_INDEX = 4 204 205 /** 206 * Return the first [GradientDrawable] found in [drawable], or null if none is found. If 207 * [drawable] is a [LayerDrawable], this will return the first layer that is a 208 * [GradientDrawable]. 209 */ findGradientDrawablenull210 private fun findGradientDrawable(drawable: Drawable): GradientDrawable? { 211 if (drawable is GradientDrawable) { 212 return drawable 213 } 214 215 if (drawable is InsetDrawable) { 216 return drawable.drawable?.let { findGradientDrawable(it) } 217 } 218 219 if (drawable is LayerDrawable) { 220 for (i in 0 until drawable.numberOfLayers) { 221 val maybeGradient = drawable.getDrawable(i) 222 if (maybeGradient is GradientDrawable) { 223 return maybeGradient 224 } 225 } 226 } 227 228 return null 229 } 230 } 231 232 private class WrappedDrawable(val wrapped: Drawable?) : Drawable() { 233 private var currentAlpha = 0xFF 234 private var previousBounds = Rect() 235 <lambda>null236 private var cornerRadii = FloatArray(8) { -1f } 237 private var previousCornerRadii = FloatArray(8) 238 drawnull239 override fun draw(canvas: Canvas) { 240 val wrapped = this.wrapped ?: return 241 242 wrapped.copyBounds(previousBounds) 243 244 wrapped.alpha = currentAlpha 245 wrapped.bounds = bounds 246 applyBackgroundRadii() 247 248 wrapped.draw(canvas) 249 250 // The background view (and therefore this drawable) is drawn before the ghost view, so 251 // the ghosted view background alpha should always be 0 when it is drawn above the 252 // background. 253 wrapped.alpha = 0 254 wrapped.bounds = previousBounds 255 restoreBackgroundRadii() 256 } 257 setAlphanull258 override fun setAlpha(alpha: Int) { 259 if (alpha != currentAlpha) { 260 currentAlpha = alpha 261 invalidateSelf() 262 } 263 } 264 getAlphanull265 override fun getAlpha() = currentAlpha 266 267 override fun getOpacity(): Int { 268 val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT 269 270 val previousAlpha = wrapped.alpha 271 wrapped.alpha = currentAlpha 272 val opacity = wrapped.opacity 273 wrapped.alpha = previousAlpha 274 return opacity 275 } 276 setColorFilternull277 override fun setColorFilter(filter: ColorFilter?) { 278 wrapped?.colorFilter = filter 279 } 280 setBackgroundRadiusnull281 fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) { 282 updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius) 283 invalidateSelf() 284 } 285 updateRadiinull286 private fun updateRadii( 287 radii: FloatArray, 288 topCornerRadius: Float, 289 bottomCornerRadius: Float 290 ) { 291 radii[0] = topCornerRadius 292 radii[1] = topCornerRadius 293 radii[2] = topCornerRadius 294 radii[3] = topCornerRadius 295 296 radii[4] = bottomCornerRadius 297 radii[5] = bottomCornerRadius 298 radii[6] = bottomCornerRadius 299 radii[7] = bottomCornerRadius 300 } 301 applyBackgroundRadiinull302 private fun applyBackgroundRadii() { 303 if (cornerRadii[0] < 0 || wrapped == null) { 304 return 305 } 306 307 savePreviousBackgroundRadii(wrapped) 308 applyBackgroundRadii(wrapped, cornerRadii) 309 } 310 savePreviousBackgroundRadiinull311 private fun savePreviousBackgroundRadii(background: Drawable) { 312 // TODO(b/184121838): This method assumes that all GradientDrawable in background will 313 // have the same radius. Should we save/restore the radii for each layer instead? 314 val gradient = findGradientDrawable(background) ?: return 315 316 // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we 317 // try to avoid that? 318 val radii = gradient.cornerRadii 319 if (radii != null) { 320 radii.copyInto(previousCornerRadii) 321 } else { 322 // Copy the cornerRadius into previousCornerRadii. 323 val radius = gradient.cornerRadius 324 updateRadii(previousCornerRadii, radius, radius) 325 } 326 } 327 applyBackgroundRadiinull328 private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) { 329 if (drawable is GradientDrawable) { 330 drawable.cornerRadii = radii 331 return 332 } 333 334 if (drawable is InsetDrawable) { 335 drawable.drawable?.let { applyBackgroundRadii(it, radii) } 336 return 337 } 338 339 if (drawable !is LayerDrawable) { 340 return 341 } 342 343 for (i in 0 until drawable.numberOfLayers) { 344 (drawable.getDrawable(i) as? GradientDrawable)?.cornerRadii = radii 345 } 346 } 347 restoreBackgroundRadiinull348 private fun restoreBackgroundRadii() { 349 if (cornerRadii[0] < 0 || wrapped == null) { 350 return 351 } 352 353 applyBackgroundRadii(wrapped, previousCornerRadii) 354 } 355 } 356 } 357