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