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