• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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