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.content.ComponentName
20 import android.graphics.Canvas
21 import android.graphics.ColorFilter
22 import android.graphics.Insets
23 import android.graphics.Matrix
24 import android.graphics.PixelFormat
25 import android.graphics.Rect
26 import android.graphics.drawable.Drawable
27 import android.graphics.drawable.GradientDrawable
28 import android.graphics.drawable.InsetDrawable
29 import android.graphics.drawable.LayerDrawable
30 import android.graphics.drawable.StateListDrawable
31 import android.util.Log
32 import android.view.GhostView
33 import android.view.View
34 import android.view.ViewGroup
35 import android.view.ViewGroupOverlay
36 import android.widget.FrameLayout
37 import com.android.internal.jank.Cuj.CujType
38 import com.android.internal.jank.InteractionJankMonitor
39 import java.util.LinkedList
40 import kotlin.math.min
41 import kotlin.math.roundToInt
42 
43 private const val TAG = "GhostedViewTransitionAnimatorController"
44 
45 /**
46  * A base implementation of [ActivityTransitionAnimator.Controller] which creates a
47  * [ghost][GhostView] of [ghostedView] as well as an expandable background view, which are drawn and
48  * animated instead of the ghosted view.
49  *
50  * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
51  * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown
52  * during this controller instantiation.
53  *
54  * Note: Avoid instantiating this directly and call [ActivityTransitionAnimator.Controller.fromView]
55  * whenever possible instead.
56  */
57 open class GhostedViewTransitionAnimatorController
58 @JvmOverloads
59 constructor(
60     /** The view that will be ghosted and from which the background will be extracted. */
61     private val ghostedView: View,
62 
63     /** The [CujType] associated to this launch animation. */
64     private val launchCujType: Int? = null,
65     override val transitionCookie: ActivityTransitionAnimator.TransitionCookie? = null,
66     override val component: ComponentName? = null,
67 
68     /** The [CujType] associated to this return animation. */
69     private val returnCujType: Int? = null,
70     private var interactionJankMonitor: InteractionJankMonitor =
71         InteractionJankMonitor.getInstance(),
72 ) : ActivityTransitionAnimator.Controller {
73     override val isLaunching: Boolean = true
74 
75     /** The container to which we will add the ghost view and expanding background. */
76     override var transitionContainer = ghostedView.rootView as ViewGroup
77     private val transitionContainerOverlay: ViewGroupOverlay
78         get() = transitionContainer.overlay
79 
80     private val transitionContainerLocation = IntArray(2)
81 
82     /** The ghost view that is drawn and animated instead of the ghosted view. */
83     private var ghostView: GhostView? = null
<lambda>null84     private val initialGhostViewMatrixValues = FloatArray(9) { 0f }
85     private val ghostViewMatrix = Matrix()
86 
87     /**
88      * The expanding background view that will be added to [transitionContainer] (below [ghostView])
89      * and animate.
90      */
91     private var backgroundView: FrameLayout? = null
92 
93     /**
94      * The drawable wrapping the [ghostedView] background and used as background for
95      * [backgroundView].
96      */
97     private var backgroundDrawable: WrappedDrawable? = null
<lambda>null98     private val backgroundInsets by lazy { background?.opticalInsets ?: Insets.NONE }
99     private var startBackgroundAlpha: Int = 0xFF
100 
101     private val ghostedViewLocation = IntArray(2)
102     private val ghostedViewState = TransitionAnimator.State()
103 
104     /**
105      * The background of the [ghostedView]. This background will be used to draw the background of
106      * the background view that is expanding up to the final animation position.
107      *
108      * Note that during the animation, the alpha value value of this background will be set to 0,
109      * then set back to its initial value at the end of the animation.
110      */
111     private val background: Drawable?
112 
113     /** CUJ identifier accounting for whether this controller is for a launch or a return. */
114     private val cujType: Int?
115         get() =
116             if (isLaunching) {
117                 launchCujType
118             } else {
119                 returnCujType
120             }
121 
122     init {
123         // Make sure the View we launch from implements LaunchableView to avoid visibility issues.
124         if (ghostedView !is LaunchableView) {
125             throw IllegalArgumentException(
126                 "A GhostedViewLaunchAnimatorController was created from a View that does not " +
127                     "implement LaunchableView. This can lead to subtle bugs where the visibility " +
128                     "of the View we are launching from is not what we expected."
129             )
130         }
131 
132         /** Find the first view with a background in [view] and its children. */
findBackgroundnull133         fun findBackground(view: View): Drawable? {
134             if (view.background != null) {
135                 return view.background
136             }
137 
138             // Perform a BFS to find the largest View with background.
139             val views = LinkedList<View>().apply { add(view) }
140 
141             while (views.isNotEmpty()) {
142                 val v = views.removeAt(0)
143                 if (v.background != null) {
144                     return v.background
145                 }
146 
147                 if (v is ViewGroup) {
148                     for (i in 0 until v.childCount) {
149                         views.add(v.getChildAt(i))
150                     }
151                 }
152             }
153 
154             return null
155         }
156 
157         background = findBackground(ghostedView)
158     }
159 
160     /**
161      * Set the corner radius of [background]. The background is the one that was returned by
162      * [getBackground].
163      */
setBackgroundCornerRadiusnull164     protected open fun setBackgroundCornerRadius(
165         background: Drawable,
166         topCornerRadius: Float,
167         bottomCornerRadius: Float
168     ) {
169         // By default, we rely on WrappedDrawable to set/restore the background radii before/after
170         // each draw.
171         backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius)
172     }
173 
174     /** Return the current top corner radius of the background. */
getCurrentTopCornerRadiusnull175     protected open fun getCurrentTopCornerRadius(): Float {
176         val drawable = background ?: return 0f
177         val gradient = findGradientDrawable(drawable) ?: return 0f
178 
179         // TODO(b/184121838): Support more than symmetric top & bottom radius.
180         val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius
181         return radius * ghostedView.scaleX
182     }
183 
184     /** Return the current bottom corner radius of the background. */
getCurrentBottomCornerRadiusnull185     protected open fun getCurrentBottomCornerRadius(): Float {
186         val drawable = background ?: return 0f
187         val gradient = findGradientDrawable(drawable) ?: return 0f
188 
189         // TODO(b/184121838): Support more than symmetric top & bottom radius.
190         val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
191         return radius * ghostedView.scaleX
192     }
193 
createAnimatorStatenull194     override fun createAnimatorState(): TransitionAnimator.State {
195         val state =
196             TransitionAnimator.State(
197                 topCornerRadius = getCurrentTopCornerRadius(),
198                 bottomCornerRadius = getCurrentBottomCornerRadius()
199             )
200         fillGhostedViewState(state)
201         return state
202     }
203 
fillGhostedViewStatenull204     fun fillGhostedViewState(state: TransitionAnimator.State) {
205         // For the animation we are interested in the area that has a non transparent background,
206         // so we have to take the optical insets into account.
207         ghostedView.getLocationOnScreen(ghostedViewLocation)
208         val insets = backgroundInsets
209         val boundCorrections: Rect =
210             if (ghostedView is LaunchableView) {
211                 ghostedView.getPaddingForLaunchAnimation()
212             } else {
213                 Rect()
214             }
215         state.top = ghostedViewLocation[1] + insets.top + boundCorrections.top
216         state.bottom =
217             ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() -
218                 insets.bottom + boundCorrections.bottom
219         state.left = ghostedViewLocation[0] + insets.left + boundCorrections.left
220         state.right =
221             ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() -
222                 insets.right + boundCorrections.right
223     }
224 
onTransitionAnimationStartnull225     override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
226         if (ghostedView.parent !is ViewGroup) {
227             // This should usually not happen, but let's make sure we don't crash if the view was
228             // detached right before we started the animation.
229             Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup")
230             return
231         }
232 
233         backgroundView =
234             FrameLayout(transitionContainer.context).also { transitionContainerOverlay.add(it) }
235 
236         // We wrap the ghosted view background and use it to draw the expandable background. Its
237         // alpha will be set to 0 as soon as we start drawing the expanding background.
238         startBackgroundAlpha = background?.alpha ?: 0xFF
239         backgroundDrawable = WrappedDrawable(background)
240         backgroundView?.background = backgroundDrawable
241 
242         // Delay the calls to `ghostedView.setVisibility()` during the animation. This must be
243         // called before `GhostView.addGhost()` is called because the latter will change the
244         // *transition* visibility, which won't be blocked and will affect the normal View
245         // visibility that is saved by `setShouldBlockVisibilityChanges()` for a later restoration.
246         (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
247 
248         // Create a ghost of the view that will be moving and fading out. This allows to fade out
249         // the content before fading out the background.
250         ghostView = GhostView.addGhost(ghostedView, transitionContainer)
251 
252         // [GhostView.addGhost], the result of which is our [ghostView], creates a [GhostView], and
253         // adds it first to a [FrameLayout] container. It then adds _that_ container to an
254         // [OverlayViewGroup]. We need to turn off clipping for that container view. Currently,
255         // however, the only way to get a reference to that overlay is by going through our
256         // [ghostView]. The [OverlayViewGroup] will always be its grandparent view.
257         // TODO(b/306652954) reference the overlay view group directly if we can
258         (ghostView?.parent?.parent as? ViewGroup)?.let {
259             it.clipChildren = false
260             it.clipToPadding = false
261         }
262 
263         val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX
264         matrix.getValues(initialGhostViewMatrixValues)
265 
266         cujType?.let { interactionJankMonitor.begin(ghostedView, it) }
267     }
268 
onTransitionAnimationProgressnull269     override fun onTransitionAnimationProgress(
270         state: TransitionAnimator.State,
271         progress: Float,
272         linearProgress: Float
273     ) {
274         val ghostView = this.ghostView ?: return
275         val backgroundView = this.backgroundView!!
276 
277         if (!state.visible || !ghostedView.isAttachedToWindow) {
278             if (ghostView.visibility == View.VISIBLE) {
279                 // Making the ghost view invisible will make the ghosted view visible, so order is
280                 // important here.
281                 ghostView.visibility = View.INVISIBLE
282 
283                 // Make the ghosted view invisible again. We use the transition visibility like
284                 // GhostView does so that we don't mess up with the accessibility tree (see
285                 // b/204944038#comment17).
286                 ghostedView.setTransitionVisibility(View.INVISIBLE)
287                 backgroundView.visibility = View.INVISIBLE
288             }
289             return
290         }
291 
292         // The ghost and backgrounds views were made invisible earlier. That can for instance happen
293         // when animating a dialog into a view.
294         if (ghostView.visibility == View.INVISIBLE) {
295             ghostView.visibility = View.VISIBLE
296             backgroundView.visibility = View.VISIBLE
297         }
298 
299         fillGhostedViewState(ghostedViewState)
300         val leftChange = state.left - ghostedViewState.left
301         val rightChange = state.right - ghostedViewState.right
302         val topChange = state.top - ghostedViewState.top
303         val bottomChange = state.bottom - ghostedViewState.bottom
304 
305         val widthRatio = state.width.toFloat() / ghostedViewState.width
306         val heightRatio = state.height.toFloat() / ghostedViewState.height
307         val scale = min(widthRatio, heightRatio)
308 
309         if (ghostedView.parent is ViewGroup) {
310             // Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted
311             // view is still attached to a ViewGroup, otherwise calculateMatrix will throw.
312             GhostView.calculateMatrix(ghostedView, transitionContainer, ghostViewMatrix)
313         }
314 
315         transitionContainer.getLocationOnScreen(transitionContainerLocation)
316         ghostViewMatrix.postScale(
317             scale,
318             scale,
319             ghostedViewState.centerX - transitionContainerLocation[0],
320             ghostedViewState.centerY - transitionContainerLocation[1]
321         )
322         ghostViewMatrix.postTranslate(
323             (leftChange + rightChange) / 2f,
324             (topChange + bottomChange) / 2f
325         )
326         ghostView.animationMatrix = ghostViewMatrix
327 
328         // We need to take into account the background insets for the background position.
329         val insets = backgroundInsets
330         val topWithInsets = state.top - insets.top
331         val leftWithInsets = state.left - insets.left
332         val rightWithInsets = state.right + insets.right
333         val bottomWithInsets = state.bottom + insets.bottom
334 
335         backgroundView.top = topWithInsets - transitionContainerLocation[1]
336         backgroundView.bottom = bottomWithInsets - transitionContainerLocation[1]
337         backgroundView.left = leftWithInsets - transitionContainerLocation[0]
338         backgroundView.right = rightWithInsets - transitionContainerLocation[0]
339 
340         val backgroundDrawable = backgroundDrawable!!
341         backgroundDrawable.wrapped?.let {
342             setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius)
343         }
344     }
345 
onTransitionAnimationEndnull346     override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
347         if (ghostView == null) {
348             // We didn't actually run the animation.
349             return
350         }
351 
352         cujType?.let { interactionJankMonitor.end(it) }
353 
354         backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
355 
356         GhostView.removeGhost(ghostedView)
357         backgroundView?.let { transitionContainerOverlay.remove(it) }
358 
359         if (ghostedView is LaunchableView) {
360             // Restore the ghosted view visibility.
361             ghostedView.setShouldBlockVisibilityChanges(false)
362             ghostedView.onActivityLaunchAnimationEnd()
363         } else {
364             // Make the ghosted view visible. We ensure that the view is considered VISIBLE by
365             // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17
366             // for more info).
367             ghostedView.visibility = View.INVISIBLE
368             ghostedView.visibility = View.VISIBLE
369             ghostedView.invalidate()
370         }
371     }
372 
373     companion object {
374         private const val CORNER_RADIUS_TOP_INDEX = 0
375         private const val CORNER_RADIUS_BOTTOM_INDEX = 4
376 
377         /**
378          * Return the first [GradientDrawable] found in [drawable], or null if none is found. If
379          * [drawable] is a [LayerDrawable], this will return the first layer that has a
380          * [GradientDrawable].
381          */
findGradientDrawablenull382         fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
383             if (drawable is GradientDrawable) {
384                 return drawable
385             }
386 
387             if (drawable is InsetDrawable) {
388                 return drawable.drawable?.let { findGradientDrawable(it) }
389             }
390 
391             if (drawable is LayerDrawable) {
392                 for (i in 0 until drawable.numberOfLayers) {
393                     val maybeGradient = findGradientDrawable(drawable.getDrawable(i))
394                     if (maybeGradient != null) {
395                         return maybeGradient
396                     }
397                 }
398             }
399 
400             if (drawable is StateListDrawable) {
401                 return findGradientDrawable(drawable.current)
402             }
403 
404             return null
405         }
406     }
407 
408     private class WrappedDrawable(val wrapped: Drawable?) : Drawable() {
409         private var currentAlpha = 0xFF
410         private var previousBounds = Rect()
411 
<lambda>null412         private var cornerRadii = FloatArray(8) { -1f }
413         private var previousCornerRadii = FloatArray(8)
414 
drawnull415         override fun draw(canvas: Canvas) {
416             val wrapped = this.wrapped ?: return
417 
418             wrapped.copyBounds(previousBounds)
419 
420             wrapped.alpha = currentAlpha
421             wrapped.bounds = bounds
422             applyBackgroundRadii()
423 
424             wrapped.draw(canvas)
425 
426             // The background view (and therefore this drawable) is drawn before the ghost view, so
427             // the ghosted view background alpha should always be 0 when it is drawn above the
428             // background.
429             wrapped.alpha = 0
430             wrapped.bounds = previousBounds
431             restoreBackgroundRadii()
432         }
433 
setAlphanull434         override fun setAlpha(alpha: Int) {
435             if (alpha != currentAlpha) {
436                 currentAlpha = alpha
437                 invalidateSelf()
438             }
439         }
440 
getAlphanull441         override fun getAlpha() = currentAlpha
442 
443         override fun getOpacity(): Int {
444             val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT
445 
446             val previousAlpha = wrapped.alpha
447             wrapped.alpha = currentAlpha
448             val opacity = wrapped.opacity
449             wrapped.alpha = previousAlpha
450             return opacity
451         }
452 
setColorFilternull453         override fun setColorFilter(filter: ColorFilter?) {
454             wrapped?.colorFilter = filter
455         }
456 
setBackgroundRadiusnull457         fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) {
458             updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius)
459             invalidateSelf()
460         }
461 
updateRadiinull462         private fun updateRadii(
463             radii: FloatArray,
464             topCornerRadius: Float,
465             bottomCornerRadius: Float
466         ) {
467             radii[0] = topCornerRadius
468             radii[1] = topCornerRadius
469             radii[2] = topCornerRadius
470             radii[3] = topCornerRadius
471 
472             radii[4] = bottomCornerRadius
473             radii[5] = bottomCornerRadius
474             radii[6] = bottomCornerRadius
475             radii[7] = bottomCornerRadius
476         }
477 
applyBackgroundRadiinull478         private fun applyBackgroundRadii() {
479             if (cornerRadii[0] < 0 || wrapped == null) {
480                 return
481             }
482 
483             savePreviousBackgroundRadii(wrapped)
484             applyBackgroundRadii(wrapped, cornerRadii)
485         }
486 
savePreviousBackgroundRadiinull487         private fun savePreviousBackgroundRadii(background: Drawable) {
488             // TODO(b/184121838): This method assumes that all GradientDrawable in background will
489             // have the same radius. Should we save/restore the radii for each layer instead?
490             val gradient = findGradientDrawable(background) ?: return
491 
492             // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we
493             // try to avoid that?
494             val radii = gradient.cornerRadii
495             if (radii != null) {
496                 radii.copyInto(previousCornerRadii)
497             } else {
498                 // Copy the cornerRadius into previousCornerRadii.
499                 val radius = gradient.cornerRadius
500                 updateRadii(previousCornerRadii, radius, radius)
501             }
502         }
503 
applyBackgroundRadiinull504         private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) {
505             if (drawable is GradientDrawable) {
506                 drawable.cornerRadii = radii
507                 return
508             }
509 
510             if (drawable is InsetDrawable) {
511                 drawable.drawable?.let { applyBackgroundRadii(it, radii) }
512                 return
513             }
514 
515             if (drawable !is LayerDrawable) {
516                 return
517             }
518 
519             for (i in 0 until drawable.numberOfLayers) {
520                 applyBackgroundRadii(drawable.getDrawable(i), radii)
521             }
522         }
523 
restoreBackgroundRadiinull524         private fun restoreBackgroundRadii() {
525             if (cornerRadii[0] < 0 || wrapped == null) {
526                 return
527             }
528 
529             applyBackgroundRadii(wrapped, previousCornerRadii)
530         }
531     }
532 }
533