• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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 com.android.systemui.Flags
40 import java.util.LinkedList
41 import kotlin.math.min
42 import kotlin.math.roundToInt
43 
44 private const val TAG = "GhostedViewTransitionAnimatorController"
45 
46 /**
47  * A base implementation of [ActivityTransitionAnimator.Controller] which creates a
48  * [ghost][GhostView] of [ghostedView] as well as an expandable background view, which are drawn and
49  * animated instead of the ghosted view.
50  *
51  * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
52  * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown
53  * during this controller instantiation.
54  *
55  * Note: Avoid instantiating this directly and call [ActivityTransitionAnimator.Controller.fromView]
56  * whenever possible instead.
57  */
58 open class GhostedViewTransitionAnimatorController
59 @JvmOverloads
60 constructor(
61     /** The view that will be ghosted and from which the background will be extracted. */
62     transitioningView: View,
63 
64     /** The [CujType] associated to this launch animation. */
65     private val launchCujType: Int? = null,
66     override val transitionCookie: ActivityTransitionAnimator.TransitionCookie? = null,
67     override val component: ComponentName? = null,
68 
69     /** The [CujType] associated to this return animation. */
70     private val returnCujType: Int? = null,
71 
72     /**
73      * Whether this controller should be invalidated after its first use, and whenever [ghostedView]
74      * is detached.
75      */
76     private val isEphemeral: Boolean = false,
77     private var interactionJankMonitor: InteractionJankMonitor =
78         InteractionJankMonitor.getInstance(),
79 
80     /** [ViewTransitionRegistry] to store the mapping of transitioning view and its token */
81     private val transitionRegistry: IViewTransitionRegistry? =
82         if (Flags.decoupleViewControllerInAnimlib()) {
83             ViewTransitionRegistry.instance
84         } else {
85             null
86         },
87 ) : ActivityTransitionAnimator.Controller {
88     override val isLaunching: Boolean = true
89 
90     /** The container to which we will add the ghost view and expanding background. */
91     override var transitionContainer: ViewGroup
92         get() = ghostedView.rootView as ViewGroup
93         set(_) {
94             // empty, should never be set to avoid memory leak
95         }
96 
97     private val transitionContainerOverlay: ViewGroupOverlay
98         get() = transitionContainer.overlay
99 
100     private val transitionContainerLocation = IntArray(2)
101 
102     /** The ghost view that is drawn and animated instead of the ghosted view. */
103     private var ghostView: GhostView? = null
<lambda>null104     private val initialGhostViewMatrixValues = FloatArray(9) { 0f }
105     private val ghostViewMatrix = Matrix()
106 
107     /**
108      * The expanding background view that will be added to [transitionContainer] (below [ghostView])
109      * and animate.
110      */
111     private var backgroundView: FrameLayout? = null
112 
113     /**
114      * The drawable wrapping the [ghostedView] background and used as background for
115      * [backgroundView].
116      */
117     private var backgroundDrawable: WrappedDrawable? = null
<lambda>null118     private val backgroundInsets by lazy { background?.opticalInsets ?: Insets.NONE }
119     private var startBackgroundAlpha: Int = 0xFF
120 
121     private val ghostedViewLocation = IntArray(2)
122     private val ghostedViewState = TransitionAnimator.State()
123 
124     /**
125      * The background of the [ghostedView]. This background will be used to draw the background of
126      * the background view that is expanding up to the final animation position.
127      *
128      * Note that during the animation, the alpha value value of this background will be set to 0,
129      * then set back to its initial value at the end of the animation.
130      */
131     private val background: Drawable?
132 
133     /** CUJ identifier accounting for whether this controller is for a launch or a return. */
134     private val cujType: Int?
135         get() =
136             if (isLaunching) {
137                 launchCujType
138             } else {
139                 returnCujType
140             }
141 
142     /**
143      * Used to automatically clean up the internal state once [ghostedView] is detached from the
144      * hierarchy.
145      */
146     private val detachListener =
147         object : View.OnAttachStateChangeListener {
onViewAttachedToWindownull148             override fun onViewAttachedToWindow(v: View) {}
149 
onViewDetachedFromWindownull150             override fun onViewDetachedFromWindow(v: View) {
151                 onDispose()
152             }
153         }
154 
155     /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */
156     private val transitionToken =
157         if (Flags.decoupleViewControllerInAnimlib()) {
158             transitionRegistry?.register(transitioningView)
159         } else {
160             null
161         }
162 
163     /** The view that will be ghosted and from which the background will be extracted */
164     private val ghostedView: View
165         get() =
166             if (Flags.decoupleViewControllerInAnimlib()) {
tokennull167                 transitionToken?.let { token -> transitionRegistry?.getView(token) }
168             } else {
169                 _ghostedView
170             }!!
171 
172     private val _ghostedView =
173         if (Flags.decoupleViewControllerInAnimlib()) {
174             null
175         } else {
176             transitioningView
177         }
178 
179     init {
180         // Make sure the View we launch from implements LaunchableView to avoid visibility issues.
181         if (transitioningView !is LaunchableView) {
182             throw IllegalArgumentException(
183                 "A GhostedViewLaunchAnimatorController was created from a View that does not " +
184                     "implement LaunchableView. This can lead to subtle bugs where the visibility " +
185                     "of the View we are launching from is not what we expected."
186             )
187         }
188 
189         /** Find the first view with a background in [view] and its children. */
findBackgroundnull190         fun findBackground(view: View): Drawable? {
191             if (view.background != null) {
192                 return view.background
193             }
194 
195             // Perform a BFS to find the largest View with background.
196             val views = LinkedList<View>().apply { add(view) }
197 
198             while (views.isNotEmpty()) {
199                 val v = views.removeAt(0)
200                 if (v.background != null) {
201                     return v.background
202                 }
203 
204                 if (v is ViewGroup) {
205                     for (i in 0 until v.childCount) {
206                         views.add(v.getChildAt(i))
207                     }
208                 }
209             }
210 
211             return null
212         }
213 
214         background = findBackground(ghostedView)
215 
216         if (TransitionAnimator.returnAnimationsEnabled() && isEphemeral) {
217             ghostedView.addOnAttachStateChangeListener(detachListener)
218         }
219     }
220 
onDisposenull221     override fun onDispose() {
222         if (TransitionAnimator.returnAnimationsEnabled()) {
223             ghostedView.removeOnAttachStateChangeListener(detachListener)
224         }
225         transitionToken?.let { token -> transitionRegistry?.unregister(token) }
226     }
227 
228     /**
229      * Set the corner radius of [background]. The background is the one that was returned by
230      * [getBackground].
231      */
setBackgroundCornerRadiusnull232     protected open fun setBackgroundCornerRadius(
233         background: Drawable,
234         topCornerRadius: Float,
235         bottomCornerRadius: Float,
236     ) {
237         // By default, we rely on WrappedDrawable to set/restore the background radii before/after
238         // each draw.
239         backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius)
240     }
241 
242     /** Return the current top corner radius of the background. */
getCurrentTopCornerRadiusnull243     protected open fun getCurrentTopCornerRadius(): Float {
244         val drawable = background ?: return 0f
245         val gradient = findGradientDrawable(drawable) ?: return 0f
246 
247         // TODO(b/184121838): Support more than symmetric top & bottom radius.
248         val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius
249         return radius * ghostedView.scaleX
250     }
251 
252     /** Return the current bottom corner radius of the background. */
getCurrentBottomCornerRadiusnull253     protected open fun getCurrentBottomCornerRadius(): Float {
254         val drawable = background ?: return 0f
255         val gradient = findGradientDrawable(drawable) ?: return 0f
256 
257         // TODO(b/184121838): Support more than symmetric top & bottom radius.
258         val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
259         return radius * ghostedView.scaleX
260     }
261 
createAnimatorStatenull262     override fun createAnimatorState(): TransitionAnimator.State {
263         val state =
264             TransitionAnimator.State(
265                 topCornerRadius = getCurrentTopCornerRadius(),
266                 bottomCornerRadius = getCurrentBottomCornerRadius(),
267             )
268         fillGhostedViewState(state)
269         return state
270     }
271 
fillGhostedViewStatenull272     fun fillGhostedViewState(state: TransitionAnimator.State) {
273         // For the animation we are interested in the area that has a non transparent background,
274         // so we have to take the optical insets into account.
275         ghostedView.getLocationOnScreen(ghostedViewLocation)
276         val insets = backgroundInsets
277         val boundCorrections: Rect =
278             if (ghostedView is LaunchableView) {
279                 (ghostedView as LaunchableView).getPaddingForLaunchAnimation()
280             } else {
281                 Rect()
282             }
283         state.top = ghostedViewLocation[1] + insets.top + boundCorrections.top
284         state.bottom =
285             ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() -
286                 insets.bottom + boundCorrections.bottom
287         state.left = ghostedViewLocation[0] + insets.left + boundCorrections.left
288         state.right =
289             ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() -
290                 insets.right + boundCorrections.right
291     }
292 
onTransitionAnimationStartnull293     override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
294         if (ghostedView.parent !is ViewGroup) {
295             // This should usually not happen, but let's make sure we don't crash if the view was
296             // detached right before we started the animation.
297             Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup")
298             return
299         }
300 
301         backgroundView =
302             FrameLayout(transitionContainer.context).also { transitionContainerOverlay.add(it) }
303 
304         // We wrap the ghosted view background and use it to draw the expandable background. Its
305         // alpha will be set to 0 as soon as we start drawing the expanding background.
306         startBackgroundAlpha = background?.alpha ?: 0xFF
307         backgroundDrawable = WrappedDrawable(background)
308         backgroundView?.background = backgroundDrawable
309 
310         // Delay the calls to `ghostedView.setVisibility()` during the animation. This must be
311         // called before `GhostView.addGhost()` is called because the latter will change the
312         // *transition* visibility, which won't be blocked and will affect the normal View
313         // visibility that is saved by `setShouldBlockVisibilityChanges()` for a later restoration.
314         (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
315 
316         try {
317             // Create a ghost of the view that will be moving and fading out. This allows to fade
318             // out the content before fading out the background.
319             ghostView = GhostView.addGhost(ghostedView, transitionContainer)
320         } catch (e: Exception) {
321             // It is not 100% clear what conditions cause this exception to happen in practice, and
322             // we could never reproduce it, but it does show up extremely rarely. We already handle
323             // the scenario where ghostView is null, so we just avoid crashing and log the error.
324             // See b/315858472 for an investigation of the issue.
325             Log.e(TAG, "Failed to create ghostView", e)
326         }
327 
328         // [GhostView.addGhost], the result of which is our [ghostView], creates a [GhostView], and
329         // adds it first to a [FrameLayout] container. It then adds _that_ container to an
330         // [OverlayViewGroup]. We need to turn off clipping for that container view. Currently,
331         // however, the only way to get a reference to that overlay is by going through our
332         // [ghostView]. The [OverlayViewGroup] will always be its grandparent view.
333         // TODO(b/306652954) reference the overlay view group directly if we can
334         (ghostView?.parent?.parent as? ViewGroup)?.let {
335             it.clipChildren = false
336             it.clipToPadding = false
337         }
338 
339         val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX
340         matrix.getValues(initialGhostViewMatrixValues)
341 
342         cujType?.let { interactionJankMonitor.begin(ghostedView, it) }
343     }
344 
onTransitionAnimationProgressnull345     override fun onTransitionAnimationProgress(
346         state: TransitionAnimator.State,
347         progress: Float,
348         linearProgress: Float,
349     ) {
350         val ghostView = this.ghostView ?: return
351         val backgroundView = this.backgroundView!!
352 
353         if (!state.visible || !ghostedView.isAttachedToWindow) {
354             if (ghostView.visibility == View.VISIBLE) {
355                 // Making the ghost view invisible will make the ghosted view visible, so order is
356                 // important here.
357                 ghostView.visibility = View.INVISIBLE
358 
359                 // Make the ghosted view invisible again. We use the transition visibility like
360                 // GhostView does so that we don't mess up with the accessibility tree (see
361                 // b/204944038#comment17).
362                 ghostedView.setTransitionVisibility(View.INVISIBLE)
363                 backgroundView.visibility = View.INVISIBLE
364             }
365             return
366         }
367 
368         // The ghost and backgrounds views were made invisible earlier. That can for instance happen
369         // when animating a dialog into a view.
370         if (ghostView.visibility == View.INVISIBLE) {
371             ghostView.visibility = View.VISIBLE
372             backgroundView.visibility = View.VISIBLE
373         }
374 
375         fillGhostedViewState(ghostedViewState)
376         val leftChange = state.left - ghostedViewState.left
377         val rightChange = state.right - ghostedViewState.right
378         val topChange = state.top - ghostedViewState.top
379         val bottomChange = state.bottom - ghostedViewState.bottom
380 
381         val widthRatio = state.width.toFloat() / ghostedViewState.width
382         val heightRatio = state.height.toFloat() / ghostedViewState.height
383         val scale = min(widthRatio, heightRatio)
384 
385         if (ghostedView.parent is ViewGroup) {
386             // Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted
387             // view is still attached to a ViewGroup, otherwise calculateMatrix will throw.
388             GhostView.calculateMatrix(ghostedView, transitionContainer, ghostViewMatrix)
389         }
390 
391         transitionContainer.getLocationOnScreen(transitionContainerLocation)
392         ghostViewMatrix.postScale(
393             scale,
394             scale,
395             ghostedViewState.centerX - transitionContainerLocation[0],
396             ghostedViewState.centerY - transitionContainerLocation[1],
397         )
398         ghostViewMatrix.postTranslate(
399             (leftChange + rightChange) / 2f,
400             (topChange + bottomChange) / 2f,
401         )
402         ghostView.animationMatrix = ghostViewMatrix
403 
404         // We need to take into account the background insets for the background position.
405         val insets = backgroundInsets
406         val topWithInsets = state.top - insets.top
407         val leftWithInsets = state.left - insets.left
408         val rightWithInsets = state.right + insets.right
409         val bottomWithInsets = state.bottom + insets.bottom
410 
411         backgroundView.top = topWithInsets - transitionContainerLocation[1]
412         backgroundView.bottom = bottomWithInsets - transitionContainerLocation[1]
413         backgroundView.left = leftWithInsets - transitionContainerLocation[0]
414         backgroundView.right = rightWithInsets - transitionContainerLocation[0]
415 
416         val backgroundDrawable = backgroundDrawable!!
417         backgroundDrawable.wrapped?.let {
418             setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius)
419         }
420     }
421 
onTransitionAnimationEndnull422     override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
423         if (ghostView == null) {
424             // We didn't actually run the animation.
425             return
426         }
427 
428         cujType?.let { interactionJankMonitor.end(it) }
429 
430         backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
431 
432         GhostView.removeGhost(ghostedView)
433         backgroundView?.let { transitionContainerOverlay.remove(it) }
434 
435         if (ghostedView is LaunchableView) {
436             // Restore the ghosted view visibility.
437             (ghostedView as LaunchableView).setShouldBlockVisibilityChanges(false)
438             (ghostedView as LaunchableView).onActivityLaunchAnimationEnd()
439         } else {
440             // Make the ghosted view visible. We ensure that the view is considered VISIBLE by
441             // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17
442             // for more info).
443             ghostedView.visibility = View.INVISIBLE
444             ghostedView.visibility = View.VISIBLE
445             ghostedView.invalidate()
446         }
447 
448         if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) {
449             onDispose()
450         }
451     }
452 
453     companion object {
454         private const val CORNER_RADIUS_TOP_INDEX = 0
455         private const val CORNER_RADIUS_BOTTOM_INDEX = 4
456 
457         /**
458          * Return the first [GradientDrawable] found in [drawable], or null if none is found. If
459          * [drawable] is a [LayerDrawable], this will return the first layer that has a
460          * [GradientDrawable].
461          */
findGradientDrawablenull462         fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
463             if (drawable is GradientDrawable) {
464                 return drawable
465             }
466 
467             if (drawable is InsetDrawable) {
468                 return drawable.drawable?.let { findGradientDrawable(it) }
469             }
470 
471             if (drawable is LayerDrawable) {
472                 for (i in 0 until drawable.numberOfLayers) {
473                     val maybeGradient = findGradientDrawable(drawable.getDrawable(i))
474                     if (maybeGradient != null) {
475                         return maybeGradient
476                     }
477                 }
478             }
479 
480             if (drawable is StateListDrawable) {
481                 return findGradientDrawable(drawable.current)
482             }
483 
484             return null
485         }
486     }
487 
488     private class WrappedDrawable(val wrapped: Drawable?) : Drawable() {
489         private var currentAlpha = 0xFF
490         private var previousBounds = Rect()
491 
<lambda>null492         private var cornerRadii = FloatArray(8) { -1f }
493         private var previousCornerRadii = FloatArray(8)
494 
drawnull495         override fun draw(canvas: Canvas) {
496             val wrapped = this.wrapped ?: return
497 
498             wrapped.copyBounds(previousBounds)
499 
500             wrapped.alpha = currentAlpha
501             wrapped.bounds = bounds
502             applyBackgroundRadii()
503 
504             wrapped.draw(canvas)
505 
506             // The background view (and therefore this drawable) is drawn before the ghost view, so
507             // the ghosted view background alpha should always be 0 when it is drawn above the
508             // background.
509             wrapped.alpha = 0
510             wrapped.bounds = previousBounds
511             restoreBackgroundRadii()
512         }
513 
setAlphanull514         override fun setAlpha(alpha: Int) {
515             if (alpha != currentAlpha) {
516                 currentAlpha = alpha
517                 invalidateSelf()
518             }
519         }
520 
getAlphanull521         override fun getAlpha() = currentAlpha
522 
523         override fun getOpacity(): Int {
524             val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT
525 
526             val previousAlpha = wrapped.alpha
527             wrapped.alpha = currentAlpha
528             val opacity = wrapped.opacity
529             wrapped.alpha = previousAlpha
530             return opacity
531         }
532 
setColorFilternull533         override fun setColorFilter(filter: ColorFilter?) {
534             wrapped?.colorFilter = filter
535         }
536 
setBackgroundRadiusnull537         fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) {
538             updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius)
539             invalidateSelf()
540         }
541 
updateRadiinull542         private fun updateRadii(
543             radii: FloatArray,
544             topCornerRadius: Float,
545             bottomCornerRadius: Float,
546         ) {
547             radii[0] = topCornerRadius
548             radii[1] = topCornerRadius
549             radii[2] = topCornerRadius
550             radii[3] = topCornerRadius
551 
552             radii[4] = bottomCornerRadius
553             radii[5] = bottomCornerRadius
554             radii[6] = bottomCornerRadius
555             radii[7] = bottomCornerRadius
556         }
557 
applyBackgroundRadiinull558         private fun applyBackgroundRadii() {
559             if (cornerRadii[0] < 0 || wrapped == null) {
560                 return
561             }
562 
563             savePreviousBackgroundRadii(wrapped)
564             applyBackgroundRadii(wrapped, cornerRadii)
565         }
566 
savePreviousBackgroundRadiinull567         private fun savePreviousBackgroundRadii(background: Drawable) {
568             // TODO(b/184121838): This method assumes that all GradientDrawable in background will
569             // have the same radius. Should we save/restore the radii for each layer instead?
570             val gradient = findGradientDrawable(background) ?: return
571 
572             // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we
573             // try to avoid that?
574             val radii = gradient.cornerRadii
575             if (radii != null) {
576                 radii.copyInto(previousCornerRadii)
577             } else {
578                 // Copy the cornerRadius into previousCornerRadii.
579                 val radius = gradient.cornerRadius
580                 updateRadii(previousCornerRadii, radius, radius)
581             }
582         }
583 
applyBackgroundRadiinull584         private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) {
585             if (drawable is GradientDrawable) {
586                 drawable.cornerRadii = radii
587                 return
588             }
589 
590             if (drawable is InsetDrawable) {
591                 drawable.drawable?.let { applyBackgroundRadii(it, radii) }
592                 return
593             }
594 
595             if (drawable !is LayerDrawable) {
596                 return
597             }
598 
599             for (i in 0 until drawable.numberOfLayers) {
600                 applyBackgroundRadii(drawable.getDrawable(i), radii)
601             }
602         }
603 
restoreBackgroundRadiinull604         private fun restoreBackgroundRadii() {
605             if (cornerRadii[0] < 0 || wrapped == null) {
606                 return
607             }
608 
609             applyBackgroundRadii(wrapped, previousCornerRadii)
610         }
611     }
612 }
613