1 /*
<lambda>null2  * Copyright (C) 2022 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.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ObjectAnimator
22 import android.animation.PropertyValuesHolder
23 import android.animation.ValueAnimator
24 import android.util.IntProperty
25 import android.view.View
26 import android.view.ViewGroup
27 import android.view.animation.Interpolator
28 import com.android.app.animation.Interpolators
29 import kotlin.math.max
30 import kotlin.math.min
31 
32 /**
33  * A class that allows changes in bounds within a view hierarchy to animate seamlessly between the
34  * start and end state.
35  */
36 class ViewHierarchyAnimator {
37     companion object {
38         /** Default values for the animation. These can all be overridden at call time. */
39         private const val DEFAULT_DURATION = 500L
40         private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD
41         private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
42         private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE
43         private val DEFAULT_FADE_IN_INTERPOLATOR = Interpolators.ALPHA_IN
44 
45         /** The properties used to animate the view bounds. */
46         private val PROPERTIES =
47             mapOf(
48                 Bound.LEFT to createViewProperty(Bound.LEFT),
49                 Bound.TOP to createViewProperty(Bound.TOP),
50                 Bound.RIGHT to createViewProperty(Bound.RIGHT),
51                 Bound.BOTTOM to createViewProperty(Bound.BOTTOM),
52             )
53 
54         private fun createViewProperty(bound: Bound): IntProperty<View> {
55             return object : IntProperty<View>(bound.label) {
56                 override fun setValue(view: View, value: Int) {
57                     setBound(view, bound, value)
58                 }
59 
60                 override fun get(view: View): Int {
61                     return getBound(view, bound) ?: bound.getValue(view)
62                 }
63             }
64         }
65 
66         /**
67          * Instruct the animator to watch for changes to the layout of [rootView] and its children
68          * and animate them. It uses the given [interpolator] and [duration].
69          *
70          * If a new layout change happens while an animation is already in progress, the animation
71          * is updated to continue from the current values to the new end state.
72          *
73          * By default, child views whole layout changes are animated as well. However, this can be
74          * controlled by [animateChildren]. If children are included, a set of [excludedViews] can
75          * be passed. If any dependent view from [rootView] matches an entry in this set, changes to
76          * that view will not be animated.
77          *
78          * The animator continues to respond to layout changes until [stopAnimating] is called.
79          *
80          * Successive calls to this method override the previous settings ([interpolator] and
81          * [duration]). The changes take effect on the next animation.
82          *
83          * Returns true if the [rootView] is already visible and will be animated, false otherwise.
84          * To animate the addition of a view, see [animateAddition].
85          */
86         @JvmOverloads
87         fun animate(
88             rootView: View,
89             interpolator: Interpolator = DEFAULT_INTERPOLATOR,
90             duration: Long = DEFAULT_DURATION,
91             animateChildren: Boolean = true,
92             excludedViews: Set<View> = emptySet(),
93         ): Boolean {
94             return animate(
95                 rootView,
96                 interpolator,
97                 duration,
98                 ephemeral = false,
99                 animateChildren = animateChildren,
100                 excludedViews = excludedViews,
101             )
102         }
103 
104         /**
105          * Like [animate], but only takes effect on the next layout update, then unregisters itself
106          * once the first animation is complete.
107          */
108         @JvmOverloads
109         fun animateNextUpdate(
110             rootView: View,
111             interpolator: Interpolator = DEFAULT_INTERPOLATOR,
112             duration: Long = DEFAULT_DURATION,
113             animateChildren: Boolean = true,
114             excludedViews: Set<View> = emptySet(),
115         ): Boolean {
116             return animate(
117                 rootView,
118                 interpolator,
119                 duration,
120                 ephemeral = true,
121                 animateChildren = animateChildren,
122                 excludedViews = excludedViews,
123             )
124         }
125 
126         private fun animate(
127             rootView: View,
128             interpolator: Interpolator,
129             duration: Long,
130             ephemeral: Boolean,
131             animateChildren: Boolean,
132             excludedViews: Set<View> = emptySet(),
133         ): Boolean {
134             if (
135                 !occupiesSpace(
136                     rootView.visibility,
137                     rootView.left,
138                     rootView.top,
139                     rootView.right,
140                     rootView.bottom,
141                 )
142             ) {
143                 return false
144             }
145 
146             val listener = createUpdateListener(interpolator, duration, ephemeral)
147             addListener(
148                 rootView,
149                 listener,
150                 recursive = true,
151                 animateChildren = animateChildren,
152                 excludedViews = excludedViews,
153             )
154             return true
155         }
156 
157         /**
158          * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation
159          * using [interpolator] and [duration].
160          *
161          * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise
162          * it keeps listening for further updates.
163          */
164         private fun createUpdateListener(
165             interpolator: Interpolator,
166             duration: Long,
167             ephemeral: Boolean,
168         ): View.OnLayoutChangeListener {
169             return createListener(interpolator, duration, ephemeral)
170         }
171 
172         /**
173          * Instruct the animator to stop watching for changes to the layout of [rootView] and its
174          * children.
175          *
176          * Any animations already in progress continue until their natural conclusion.
177          */
178         fun stopAnimating(rootView: View) {
179             recursivelyRemoveListener(rootView)
180         }
181 
182         /**
183          * Instruct the animator to watch for changes to the layout of [rootView] and its children,
184          * and animate the next time the hierarchy appears after not being visible. It uses the
185          * given [interpolator] and [duration].
186          *
187          * The start state of the animation is controlled by [origin]. This value can be any of the
188          * four corners, any of the four edges, or the center of the view. If any margins are added
189          * on the side(s) of the origin, the translation of those margins can be included by
190          * specifying [includeMargins].
191          *
192          * Returns true if the [rootView] is invisible and will be animated, false otherwise. To
193          * animate an already visible view, see [animate] and [animateNextUpdate].
194          *
195          * Then animator unregisters itself once the first addition animation is complete.
196          *
197          * @param includeFadeIn true if the animator should also fade in the view and child views.
198          * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if
199          *   [includeFadeIn] is false.
200          * @param onAnimationEnd an optional runnable that will be run once the animation finishes,
201          *   regardless of whether the animation is cancelled or finishes successfully.
202          */
203         @JvmOverloads
204         fun animateAddition(
205             rootView: View,
206             origin: Hotspot = Hotspot.CENTER,
207             interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR,
208             duration: Long = DEFAULT_DURATION,
209             includeMargins: Boolean = false,
210             includeFadeIn: Boolean = false,
211             fadeInInterpolator: Interpolator = DEFAULT_FADE_IN_INTERPOLATOR,
212             onAnimationEnd: Runnable? = null,
213         ): Boolean {
214             if (
215                 occupiesSpace(
216                     rootView.visibility,
217                     rootView.left,
218                     rootView.top,
219                     rootView.right,
220                     rootView.bottom,
221                 )
222             ) {
223                 return false
224             }
225 
226             val listener =
227                 createAdditionListener(
228                     origin,
229                     interpolator,
230                     duration,
231                     ignorePreviousValues = !includeMargins,
232                     onAnimationEnd,
233                 )
234             addListener(rootView, listener, recursive = true)
235 
236             if (!includeFadeIn) {
237                 return true
238             }
239 
240             if (rootView is ViewGroup) {
241                 // First, fade in the container view
242                 val containerDuration = duration / 6
243                 createAndStartFadeInAnimator(
244                     rootView,
245                     containerDuration,
246                     startDelay = 0,
247                     interpolator = fadeInInterpolator,
248                 )
249 
250                 // Then, fade in the child views
251                 val childDuration = duration / 3
252                 for (i in 0 until rootView.childCount) {
253                     val view = rootView.getChildAt(i)
254                     createAndStartFadeInAnimator(
255                         view,
256                         childDuration,
257                         // Wait until the container fades in before fading in the children
258                         startDelay = containerDuration,
259                         interpolator = fadeInInterpolator,
260                     )
261                 }
262                 // For now, we don't recursively fade in additional sub views (e.g. grandchild
263                 // views) since it hasn't been necessary, but we could add that functionality.
264             } else {
265                 // Fade in the view during the first half of the addition
266                 createAndStartFadeInAnimator(
267                     rootView,
268                     duration / 2,
269                     startDelay = 0,
270                     interpolator = fadeInInterpolator,
271                 )
272             }
273 
274             return true
275         }
276 
277         /**
278          * Returns a new [View.OnLayoutChangeListener] that on the next call triggers a layout
279          * addition animation from the given [origin], using [interpolator] and [duration].
280          *
281          * If [ignorePreviousValues] is true, the animation will only span the area covered by the
282          * new bounds. Otherwise it will include the margins between the previous and new bounds.
283          */
284         private fun createAdditionListener(
285             origin: Hotspot,
286             interpolator: Interpolator,
287             duration: Long,
288             ignorePreviousValues: Boolean,
289             onAnimationEnd: Runnable? = null,
290         ): View.OnLayoutChangeListener {
291             return createListener(
292                 interpolator,
293                 duration,
294                 ephemeral = true,
295                 origin = origin,
296                 ignorePreviousValues = ignorePreviousValues,
297                 onAnimationEnd,
298             )
299         }
300 
301         /**
302          * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation
303          * using [interpolator] and [duration].
304          *
305          * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise
306          * it keeps listening for further updates.
307          *
308          * [origin] specifies whether the start values should be determined by a hotspot, and
309          * [ignorePreviousValues] controls whether the previous values should be taken into account.
310          */
311         private fun createListener(
312             interpolator: Interpolator,
313             duration: Long,
314             ephemeral: Boolean,
315             origin: Hotspot? = null,
316             ignorePreviousValues: Boolean = false,
317             onAnimationEnd: Runnable? = null,
318         ): View.OnLayoutChangeListener {
319             return object : View.OnLayoutChangeListener {
320                 override fun onLayoutChange(
321                     view: View?,
322                     left: Int,
323                     top: Int,
324                     right: Int,
325                     bottom: Int,
326                     previousLeft: Int,
327                     previousTop: Int,
328                     previousRight: Int,
329                     previousBottom: Int,
330                 ) {
331                     if (view == null) return
332 
333                     val startLeft = getBound(view, Bound.LEFT) ?: previousLeft
334                     val startTop = getBound(view, Bound.TOP) ?: previousTop
335                     val startRight = getBound(view, Bound.RIGHT) ?: previousRight
336                     val startBottom = getBound(view, Bound.BOTTOM) ?: previousBottom
337 
338                     (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
339 
340                     if (!occupiesSpace(view.visibility, left, top, right, bottom)) {
341                         setBound(view, Bound.LEFT, left)
342                         setBound(view, Bound.TOP, top)
343                         setBound(view, Bound.RIGHT, right)
344                         setBound(view, Bound.BOTTOM, bottom)
345                         return
346                     }
347 
348                     val startValues =
349                         processStartValues(
350                             origin,
351                             left,
352                             top,
353                             right,
354                             bottom,
355                             startLeft,
356                             startTop,
357                             startRight,
358                             startBottom,
359                             ignorePreviousValues,
360                         )
361                     val endValues =
362                         mapOf(
363                             Bound.LEFT to left,
364                             Bound.TOP to top,
365                             Bound.RIGHT to right,
366                             Bound.BOTTOM to bottom,
367                         )
368 
369                     val boundsToAnimate = mutableSetOf<Bound>()
370                     if (startValues.getValue(Bound.LEFT) != left) boundsToAnimate.add(Bound.LEFT)
371                     if (startValues.getValue(Bound.TOP) != top) boundsToAnimate.add(Bound.TOP)
372                     if (startValues.getValue(Bound.RIGHT) != right) boundsToAnimate.add(Bound.RIGHT)
373                     if (startValues.getValue(Bound.BOTTOM) != bottom) {
374                         boundsToAnimate.add(Bound.BOTTOM)
375                     }
376 
377                     if (boundsToAnimate.isNotEmpty()) {
378                         startAnimation(
379                             view,
380                             boundsToAnimate,
381                             startValues,
382                             endValues,
383                             interpolator,
384                             duration,
385                             ephemeral,
386                             onAnimationEnd,
387                         )
388                     }
389                 }
390             }
391         }
392 
393         /**
394          * Animates the removal of [rootView] and its children from the hierarchy. It uses the given
395          * [interpolator] and [duration].
396          *
397          * The end state of the animation is controlled by [destination]. This value can be any of
398          * the four corners, any of the four edges, or the center of the view. If any margins are
399          * added on the side(s) of the [destination], the translation of those margins can be
400          * included by specifying [includeMargins].
401          *
402          * @param onAnimationEnd an optional runnable that will be run once the animation finishes,
403          *   regardless of whether the animation is cancelled or finishes successfully.
404          */
405         @JvmOverloads
406         fun animateRemoval(
407             rootView: View,
408             destination: Hotspot = Hotspot.CENTER,
409             interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
410             duration: Long = DEFAULT_DURATION,
411             includeMargins: Boolean = false,
412             onAnimationEnd: Runnable? = null,
413         ): Boolean {
414             if (
415                 !occupiesSpace(
416                     rootView.visibility,
417                     rootView.left,
418                     rootView.top,
419                     rootView.right,
420                     rootView.bottom,
421                 )
422             ) {
423                 return false
424             }
425 
426             val parent = rootView.parent as ViewGroup
427 
428             // Ensure that rootView's siblings animate nicely around the removal.
429             val listener = createUpdateListener(interpolator, duration, ephemeral = true)
430             for (i in 0 until parent.childCount) {
431                 val child = parent.getChildAt(i)
432                 if (child == rootView) continue
433                 addListener(child, listener, recursive = false)
434             }
435 
436             val viewHasSiblings = parent.childCount > 1
437             if (viewHasSiblings) {
438                 // Remove the view so that a layout update is triggered for the siblings and they
439                 // animate to their next position while the view's removal is also animating.
440                 parent.removeView(rootView)
441                 // By adding the view to the overlay, we can animate it while it isn't part of the
442                 // view hierarchy. It is correctly positioned because we have its previous bounds,
443                 // and we set them manually during the animation.
444                 parent.overlay.add(rootView)
445             }
446             // If this view has no siblings, the parent view may shrink to (0,0) size and mess
447             // up the animation if we immediately remove the view. So instead, we just leave the
448             // view in the real hierarchy until the animation finishes.
449 
450             val endRunnable = Runnable {
451                 if (viewHasSiblings) {
452                     parent.overlay.remove(rootView)
453                 } else {
454                     parent.removeView(rootView)
455                 }
456                 onAnimationEnd?.run()
457             }
458 
459             val startValues =
460                 mapOf(
461                     Bound.LEFT to rootView.left,
462                     Bound.TOP to rootView.top,
463                     Bound.RIGHT to rootView.right,
464                     Bound.BOTTOM to rootView.bottom,
465                 )
466             val endValues =
467                 processEndValuesForRemoval(
468                     destination,
469                     rootView,
470                     rootView.left,
471                     rootView.top,
472                     rootView.right,
473                     rootView.bottom,
474                     includeMargins,
475                 )
476 
477             val boundsToAnimate = mutableSetOf<Bound>()
478             if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT)
479             if (rootView.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP)
480             if (rootView.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT)
481             if (rootView.bottom != endValues.getValue(Bound.BOTTOM)) {
482                 boundsToAnimate.add(Bound.BOTTOM)
483             }
484 
485             startAnimation(
486                 rootView,
487                 boundsToAnimate,
488                 startValues,
489                 endValues,
490                 interpolator,
491                 duration,
492                 ephemeral = true,
493                 endRunnable,
494             )
495 
496             if (rootView is ViewGroup) {
497                 // Shift the children so they maintain a consistent position within the shrinking
498                 // view.
499                 shiftChildrenForRemoval(rootView, destination, endValues, interpolator, duration)
500 
501                 // Fade out the children during the first half of the removal, so they don't clutter
502                 // too much once the view becomes very small. Then we fade out the view itself, in
503                 // case it has its own content and/or background.
504                 val startAlphas = FloatArray(rootView.childCount)
505                 for (i in 0 until rootView.childCount) {
506                     startAlphas[i] = rootView.getChildAt(i).alpha
507                 }
508 
509                 val animator = ValueAnimator.ofFloat(1f, 0f)
510                 animator.interpolator = Interpolators.ALPHA_OUT
511                 animator.duration = duration / 2
512                 animator.addUpdateListener { animation ->
513                     for (i in 0 until rootView.childCount) {
514                         rootView.getChildAt(i).alpha =
515                             (animation.animatedValue as Float) * startAlphas[i]
516                     }
517                 }
518                 animator.addListener(
519                     object : AnimatorListenerAdapter() {
520                         override fun onAnimationEnd(animation: Animator) {
521                             rootView
522                                 .animate()
523                                 .alpha(0f)
524                                 .setInterpolator(Interpolators.ALPHA_OUT)
525                                 .setDuration(duration / 2)
526                                 .start()
527                         }
528                     }
529                 )
530                 animator.start()
531             } else {
532                 // Fade out the view during the second half of the removal.
533                 rootView
534                     .animate()
535                     .alpha(0f)
536                     .setInterpolator(Interpolators.ALPHA_OUT)
537                     .setDuration(duration / 2)
538                     .setStartDelay(duration / 2)
539                     .start()
540             }
541 
542             return true
543         }
544 
545         /**
546          * Animates the children of [rootView] so that its layout remains internally consistent as
547          * it shrinks towards [destination] and changes its bounds to [endValues].
548          *
549          * Uses [interpolator] and [duration], which should match those of the removal animation.
550          */
551         private fun shiftChildrenForRemoval(
552             rootView: ViewGroup,
553             destination: Hotspot,
554             endValues: Map<Bound, Int>,
555             interpolator: Interpolator,
556             duration: Long,
557         ) {
558             for (i in 0 until rootView.childCount) {
559                 val child = rootView.getChildAt(i)
560                 val childStartValues =
561                     mapOf(
562                         Bound.LEFT to child.left,
563                         Bound.TOP to child.top,
564                         Bound.RIGHT to child.right,
565                         Bound.BOTTOM to child.bottom,
566                     )
567                 val childEndValues =
568                     processChildEndValuesForRemoval(
569                         destination,
570                         child.left,
571                         child.top,
572                         child.right,
573                         child.bottom,
574                         endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT),
575                         endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP),
576                     )
577 
578                 val boundsToAnimate = mutableSetOf<Bound>()
579                 if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT)
580                 if (child.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP)
581                 if (child.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT)
582                 if (child.bottom != endValues.getValue(Bound.BOTTOM)) {
583                     boundsToAnimate.add(Bound.BOTTOM)
584                 }
585 
586                 startAnimation(
587                     child,
588                     boundsToAnimate,
589                     childStartValues,
590                     childEndValues,
591                     interpolator,
592                     duration,
593                     ephemeral = true,
594                 )
595             }
596         }
597 
598         /**
599          * Returns whether the given [visibility] and bounds are consistent with a view being a
600          * contributing part of the hierarchy.
601          */
602         private fun occupiesSpace(
603             visibility: Int,
604             left: Int,
605             top: Int,
606             right: Int,
607             bottom: Int,
608         ): Boolean {
609             return visibility != View.GONE && left != right && top != bottom
610         }
611 
612         /**
613          * Computes the actual starting values based on the requested [origin] and on
614          * [ignorePreviousValues].
615          *
616          * If [origin] is null, the resolved start values will be the same as those passed in, or
617          * the same as the new values if [ignorePreviousValues] is true. If [origin] is not null,
618          * the start values are resolved based on it, and [ignorePreviousValues] controls whether or
619          * not newly introduced margins are included.
620          *
621          * Base case
622          *
623          * ```
624          *     1) origin=TOP
625          *         x---------x    x---------x    x---------x    x---------x    x---------x
626          *                        x---------x    |         |    |         |    |         |
627          *                     ->             -> x---------x -> |         | -> |         |
628          *                                                      x---------x    |         |
629          *                                                                     x---------x
630          *     2) origin=BOTTOM_LEFT
631          *                                                                     x---------x
632          *                                                      x-------x      |         |
633          *                     ->             -> x----x      -> |       |   -> |         |
634          *                        x--x           |    |         |       |      |         |
635          *         x              x--x           x----x         x-------x      x---------x
636          *     3) origin=CENTER
637          *                                                                     x---------x
638          *                                         x-----x       x-------x     |         |
639          *              x      ->    x---x    ->   |     |   ->  |       |  -> |         |
640          *                                         x-----x       x-------x     |         |
641          *                                                                     x---------x
642          * ```
643          *
644          * In case the start and end values differ in the direction of the origin, and
645          * [ignorePreviousValues] is false, the previous values are used and a translation is
646          * included in addition to the view expansion.
647          *
648          * ```
649          *     origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70)
650          *         x
651          *                         x--x
652          *                         x--x            x----x
653          *                     ->             ->   |    |    ->    x------x
654          *                                         x----x          |      |
655          *                                                         |      |
656          *                                                         x------x
657          * ```
658          */
659         private fun processStartValues(
660             origin: Hotspot?,
661             newLeft: Int,
662             newTop: Int,
663             newRight: Int,
664             newBottom: Int,
665             previousLeft: Int,
666             previousTop: Int,
667             previousRight: Int,
668             previousBottom: Int,
669             ignorePreviousValues: Boolean,
670         ): Map<Bound, Int> {
671             val startLeft = if (ignorePreviousValues) newLeft else previousLeft
672             val startTop = if (ignorePreviousValues) newTop else previousTop
673             val startRight = if (ignorePreviousValues) newRight else previousRight
674             val startBottom = if (ignorePreviousValues) newBottom else previousBottom
675 
676             var left = startLeft
677             var top = startTop
678             var right = startRight
679             var bottom = startBottom
680 
681             if (origin != null) {
682                 left =
683                     when (origin) {
684                         Hotspot.CENTER -> (newLeft + newRight) / 2
685                         Hotspot.BOTTOM_LEFT,
686                         Hotspot.LEFT,
687                         Hotspot.TOP_LEFT -> min(startLeft, newLeft)
688                         Hotspot.TOP,
689                         Hotspot.BOTTOM -> newLeft
690                         Hotspot.TOP_RIGHT,
691                         Hotspot.RIGHT,
692                         Hotspot.BOTTOM_RIGHT -> max(startRight, newRight)
693                     }
694                 top =
695                     when (origin) {
696                         Hotspot.CENTER -> (newTop + newBottom) / 2
697                         Hotspot.TOP_LEFT,
698                         Hotspot.TOP,
699                         Hotspot.TOP_RIGHT -> min(startTop, newTop)
700                         Hotspot.LEFT,
701                         Hotspot.RIGHT -> newTop
702                         Hotspot.BOTTOM_RIGHT,
703                         Hotspot.BOTTOM,
704                         Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom)
705                     }
706                 right =
707                     when (origin) {
708                         Hotspot.CENTER -> (newLeft + newRight) / 2
709                         Hotspot.TOP_RIGHT,
710                         Hotspot.RIGHT,
711                         Hotspot.BOTTOM_RIGHT -> max(startRight, newRight)
712                         Hotspot.TOP,
713                         Hotspot.BOTTOM -> newRight
714                         Hotspot.BOTTOM_LEFT,
715                         Hotspot.LEFT,
716                         Hotspot.TOP_LEFT -> min(startLeft, newLeft)
717                     }
718                 bottom =
719                     when (origin) {
720                         Hotspot.CENTER -> (newTop + newBottom) / 2
721                         Hotspot.BOTTOM_RIGHT,
722                         Hotspot.BOTTOM,
723                         Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom)
724                         Hotspot.LEFT,
725                         Hotspot.RIGHT -> newBottom
726                         Hotspot.TOP_LEFT,
727                         Hotspot.TOP,
728                         Hotspot.TOP_RIGHT -> min(startTop, newTop)
729                     }
730             }
731 
732             return mapOf(
733                 Bound.LEFT to left,
734                 Bound.TOP to top,
735                 Bound.RIGHT to right,
736                 Bound.BOTTOM to bottom,
737             )
738         }
739 
740         /**
741          * Computes a removal animation's end values based on the requested [destination] and the
742          * view's starting bounds.
743          *
744          * Examples:
745          * ```
746          *     1) destination=TOP
747          *         x---------x    x---------x    x---------x    x---------x    x---------x
748          *         |         |    |         |    |         |    x---------x
749          *         |         | -> |         | -> x---------x ->             ->
750          *         |         |    x---------x
751          *         x---------x
752          *      2) destination=BOTTOM_LEFT
753          *         x---------x
754          *         |         |    x-------x
755          *         |         | -> |       |   -> x----x      ->             ->
756          *         |         |    |       |      |    |         x--x
757          *         x---------x    x-------x      x----x         x--x           x
758          *     3) destination=CENTER
759          *         x---------x
760          *         |         |     x-------x       x-----x
761          *         |         | ->  |       |  ->   |     |   ->    x---x    ->      x
762          *         |         |     x-------x       x-----x
763          *         x---------x
764          *     4) destination=TOP, includeMargins=true (and view has large top margin)
765          *                                                                     x---------x
766          *                                                      x---------x
767          *                                       x---------x    x---------x
768          *                        x---------x    |         |
769          *         x---------x    |         |    x---------x
770          *         |         |    |         |
771          *         |         | -> x---------x ->             ->             ->
772          *         |         |
773          *         x---------x
774          * ```
775          */
776         private fun processEndValuesForRemoval(
777             destination: Hotspot,
778             rootView: View,
779             left: Int,
780             top: Int,
781             right: Int,
782             bottom: Int,
783             includeMargins: Boolean = false,
784         ): Map<Bound, Int> {
785             val marginAdjustment =
786                 if (includeMargins && (rootView.layoutParams is ViewGroup.MarginLayoutParams)) {
787                     val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams
788                     DimenHolder(
789                         left = marginLp.leftMargin,
790                         top = marginLp.topMargin,
791                         right = marginLp.rightMargin,
792                         bottom = marginLp.bottomMargin,
793                     )
794                 } else {
795                     DimenHolder(0, 0, 0, 0)
796                 }
797 
798             // These are the end values to use *if* this bound is part of the destination.
799             val endLeft = left - marginAdjustment.left
800             val endTop = top - marginAdjustment.top
801             val endRight = right + marginAdjustment.right
802             val endBottom = bottom + marginAdjustment.bottom
803 
804             // For the below calculations: We need to ensure that the destination bound and the
805             // bound *opposite* to the destination bound end at the same value, to ensure that the
806             // view has size 0 for that dimension.
807             // For example,
808             //  - If destination=TOP, then endTop == endBottom. Left and right stay the same.
809             //  - If destination=RIGHT, then endRight == endLeft. Top and bottom stay the same.
810             //  - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight.
811 
812             return when (destination) {
813                 Hotspot.TOP ->
814                     mapOf(
815                         Bound.TOP to endTop,
816                         Bound.BOTTOM to endTop,
817                         Bound.LEFT to left,
818                         Bound.RIGHT to right,
819                     )
820                 Hotspot.TOP_RIGHT ->
821                     mapOf(
822                         Bound.TOP to endTop,
823                         Bound.BOTTOM to endTop,
824                         Bound.RIGHT to endRight,
825                         Bound.LEFT to endRight,
826                     )
827                 Hotspot.RIGHT ->
828                     mapOf(
829                         Bound.RIGHT to endRight,
830                         Bound.LEFT to endRight,
831                         Bound.TOP to top,
832                         Bound.BOTTOM to bottom,
833                     )
834                 Hotspot.BOTTOM_RIGHT ->
835                     mapOf(
836                         Bound.BOTTOM to endBottom,
837                         Bound.TOP to endBottom,
838                         Bound.RIGHT to endRight,
839                         Bound.LEFT to endRight,
840                     )
841                 Hotspot.BOTTOM ->
842                     mapOf(
843                         Bound.BOTTOM to endBottom,
844                         Bound.TOP to endBottom,
845                         Bound.LEFT to left,
846                         Bound.RIGHT to right,
847                     )
848                 Hotspot.BOTTOM_LEFT ->
849                     mapOf(
850                         Bound.BOTTOM to endBottom,
851                         Bound.TOP to endBottom,
852                         Bound.LEFT to endLeft,
853                         Bound.RIGHT to endLeft,
854                     )
855                 Hotspot.LEFT ->
856                     mapOf(
857                         Bound.LEFT to endLeft,
858                         Bound.RIGHT to endLeft,
859                         Bound.TOP to top,
860                         Bound.BOTTOM to bottom,
861                     )
862                 Hotspot.TOP_LEFT ->
863                     mapOf(
864                         Bound.TOP to endTop,
865                         Bound.BOTTOM to endTop,
866                         Bound.LEFT to endLeft,
867                         Bound.RIGHT to endLeft,
868                     )
869                 Hotspot.CENTER ->
870                     mapOf(
871                         Bound.LEFT to (endLeft + endRight) / 2,
872                         Bound.RIGHT to (endLeft + endRight) / 2,
873                         Bound.TOP to (endTop + endBottom) / 2,
874                         Bound.BOTTOM to (endTop + endBottom) / 2,
875                     )
876             }
877         }
878 
879         /**
880          * Computes the end values for the child of a view being removed, based on the child's
881          * starting bounds, the removal's [destination], and the [parentWidth] and [parentHeight].
882          *
883          * The end values always represent the child's position after it has been translated so that
884          * its center is at the [destination].
885          *
886          * Examples:
887          * ```
888          *     1) destination=TOP
889          *         The child maintains its left and right positions, but is shifted up so that its
890          *         center is on the parent's end top edge.
891          *     2) destination=BOTTOM_LEFT
892          *         The child shifts so that its center is on the parent's end bottom left corner.
893          *     3) destination=CENTER
894          *         The child shifts so that its own center is on the parent's end center.
895          * ```
896          */
897         private fun processChildEndValuesForRemoval(
898             destination: Hotspot,
899             left: Int,
900             top: Int,
901             right: Int,
902             bottom: Int,
903             parentWidth: Int,
904             parentHeight: Int,
905         ): Map<Bound, Int> {
906             val halfWidth = (right - left) / 2
907             val halfHeight = (bottom - top) / 2
908 
909             val endLeft =
910                 when (destination) {
911                     Hotspot.CENTER -> (parentWidth / 2) - halfWidth
912                     Hotspot.BOTTOM_LEFT,
913                     Hotspot.LEFT,
914                     Hotspot.TOP_LEFT -> -halfWidth
915                     Hotspot.TOP_RIGHT,
916                     Hotspot.RIGHT,
917                     Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth
918                     Hotspot.TOP,
919                     Hotspot.BOTTOM -> left
920                 }
921             val endTop =
922                 when (destination) {
923                     Hotspot.CENTER -> (parentHeight / 2) - halfHeight
924                     Hotspot.TOP_LEFT,
925                     Hotspot.TOP,
926                     Hotspot.TOP_RIGHT -> -halfHeight
927                     Hotspot.BOTTOM_RIGHT,
928                     Hotspot.BOTTOM,
929                     Hotspot.BOTTOM_LEFT -> parentHeight - halfHeight
930                     Hotspot.LEFT,
931                     Hotspot.RIGHT -> top
932                 }
933             val endRight =
934                 when (destination) {
935                     Hotspot.CENTER -> (parentWidth / 2) + halfWidth
936                     Hotspot.TOP_RIGHT,
937                     Hotspot.RIGHT,
938                     Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth
939                     Hotspot.BOTTOM_LEFT,
940                     Hotspot.LEFT,
941                     Hotspot.TOP_LEFT -> halfWidth
942                     Hotspot.TOP,
943                     Hotspot.BOTTOM -> right
944                 }
945             val endBottom =
946                 when (destination) {
947                     Hotspot.CENTER -> (parentHeight / 2) + halfHeight
948                     Hotspot.BOTTOM_RIGHT,
949                     Hotspot.BOTTOM,
950                     Hotspot.BOTTOM_LEFT -> parentHeight + halfHeight
951                     Hotspot.TOP_LEFT,
952                     Hotspot.TOP,
953                     Hotspot.TOP_RIGHT -> halfHeight
954                     Hotspot.LEFT,
955                     Hotspot.RIGHT -> bottom
956                 }
957 
958             return mapOf(
959                 Bound.LEFT to endLeft,
960                 Bound.TOP to endTop,
961                 Bound.RIGHT to endRight,
962                 Bound.BOTTOM to endBottom,
963             )
964         }
965 
966         private fun addListener(
967             view: View,
968             listener: View.OnLayoutChangeListener,
969             recursive: Boolean = false,
970             animateChildren: Boolean = true,
971             excludedViews: Set<View> = emptySet(),
972         ) {
973             if (excludedViews.contains(view)) return
974 
975             // Make sure that only one listener is active at a time.
976             val previousListener = view.getTag(R.id.tag_layout_listener)
977             if (previousListener != null && previousListener is View.OnLayoutChangeListener) {
978                 view.removeOnLayoutChangeListener(previousListener)
979             }
980 
981             view.addOnLayoutChangeListener(listener)
982             view.setTag(R.id.tag_layout_listener, listener)
983             if (animateChildren && view is ViewGroup && recursive) {
984                 for (i in 0 until view.childCount) {
985                     addListener(
986                         view.getChildAt(i),
987                         listener,
988                         recursive = true,
989                         animateChildren = animateChildren,
990                         excludedViews = excludedViews,
991                     )
992                 }
993             }
994         }
995 
996         private fun recursivelyRemoveListener(view: View) {
997             val listener = view.getTag(R.id.tag_layout_listener)
998             if (listener != null && listener is View.OnLayoutChangeListener) {
999                 view.setTag(R.id.tag_layout_listener, null /* tag */)
1000                 view.removeOnLayoutChangeListener(listener)
1001             }
1002 
1003             if (view is ViewGroup) {
1004                 for (i in 0 until view.childCount) {
1005                     recursivelyRemoveListener(view.getChildAt(i))
1006                 }
1007             }
1008         }
1009 
1010         private fun getBound(view: View, bound: Bound): Int? {
1011             return view.getTag(bound.overrideTag) as? Int
1012         }
1013 
1014         private fun setBound(view: View, bound: Bound, value: Int) {
1015             view.setTag(bound.overrideTag, value)
1016             bound.setValue(view, value)
1017         }
1018 
1019         /**
1020          * Initiates the animation of the requested [bounds] between [startValues] and [endValues]
1021          * by creating the animator, registering it with the [view], and starting it using
1022          * [interpolator] and [duration].
1023          *
1024          * If [ephemeral] is true, the layout change listener is unregistered at the end of the
1025          * animation, so no more animations happen.
1026          */
1027         private fun startAnimation(
1028             view: View,
1029             bounds: Set<Bound>,
1030             startValues: Map<Bound, Int>,
1031             endValues: Map<Bound, Int>,
1032             interpolator: Interpolator,
1033             duration: Long,
1034             ephemeral: Boolean,
1035             onAnimationEnd: Runnable? = null,
1036         ) {
1037             val propertyValuesHolders =
1038                 buildList {
1039                         bounds.forEach { bound ->
1040                             add(
1041                                 PropertyValuesHolder.ofInt(
1042                                     PROPERTIES[bound],
1043                                     startValues.getValue(bound),
1044                                     endValues.getValue(bound),
1045                                 )
1046                             )
1047                         }
1048                     }
1049                     .toTypedArray()
1050 
1051             (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
1052 
1053             val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
1054             animator.interpolator = interpolator
1055             animator.duration = duration
1056             animator.addListener(
1057                 object : AnimatorListenerAdapter() {
1058                     var cancelled = false
1059 
1060                     override fun onAnimationEnd(animation: Animator) {
1061                         view.setTag(R.id.tag_animator, null /* tag */)
1062                         bounds.forEach { view.setTag(it.overrideTag, null /* tag */) }
1063 
1064                         // When an animation is cancelled, a new one might be taking over. We
1065                         // shouldn't unregister the listener yet.
1066                         if (ephemeral && !cancelled) {
1067                             // The duration is the same for the whole hierarchy, so it's safe to
1068                             // remove the listener recursively. We do this because some descendant
1069                             // views might not change bounds, and therefore not animate and leak the
1070                             // listener.
1071                             recursivelyRemoveListener(view)
1072                         }
1073                         // Run the end runnable regardless of whether the animation was cancelled or
1074                         // not - this ensures critical actions (like removing a window) always occur
1075                         // (see b/344049884).
1076                         onAnimationEnd?.run()
1077                     }
1078 
1079                     override fun onAnimationCancel(animation: Animator) {
1080                         cancelled = true
1081                     }
1082                 }
1083             )
1084 
1085             bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) }
1086 
1087             view.setTag(R.id.tag_animator, animator)
1088             animator.start()
1089         }
1090 
1091         private fun createAndStartFadeInAnimator(
1092             view: View,
1093             duration: Long,
1094             startDelay: Long,
1095             interpolator: Interpolator,
1096         ) {
1097             val animator = ObjectAnimator.ofFloat(view, "alpha", 1f)
1098             animator.startDelay = startDelay
1099             animator.duration = duration
1100             animator.interpolator = interpolator
1101             animator.addListener(
1102                 object : AnimatorListenerAdapter() {
1103                     override fun onAnimationEnd(animation: Animator) {
1104                         view.setTag(R.id.tag_alpha_animator, null /* tag */)
1105                     }
1106                 }
1107             )
1108 
1109             (view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel()
1110             view.setTag(R.id.tag_alpha_animator, animator)
1111             animator.start()
1112         }
1113     }
1114 
1115     /** An enum used to determine the origin of addition animations. */
1116     enum class Hotspot {
1117         CENTER,
1118         LEFT,
1119         TOP_LEFT,
1120         TOP,
1121         TOP_RIGHT,
1122         RIGHT,
1123         BOTTOM_RIGHT,
1124         BOTTOM,
1125         BOTTOM_LEFT,
1126     }
1127 
1128     private enum class Bound(val label: String, val overrideTag: Int) {
1129         LEFT("left", R.id.tag_override_left) {
1130             override fun setValue(view: View, value: Int) {
1131                 view.left = value
1132             }
1133 
1134             override fun getValue(view: View): Int {
1135                 return view.left
1136             }
1137         },
1138         TOP("top", R.id.tag_override_top) {
1139             override fun setValue(view: View, value: Int) {
1140                 view.top = value
1141             }
1142 
1143             override fun getValue(view: View): Int {
1144                 return view.top
1145             }
1146         },
1147         RIGHT("right", R.id.tag_override_right) {
1148             override fun setValue(view: View, value: Int) {
1149                 view.right = value
1150             }
1151 
1152             override fun getValue(view: View): Int {
1153                 return view.right
1154             }
1155         },
1156         BOTTOM("bottom", R.id.tag_override_bottom) {
1157             override fun setValue(view: View, value: Int) {
1158                 view.bottom = value
1159             }
1160 
1161             override fun getValue(view: View): Int {
1162                 return view.bottom
1163             }
1164         };
1165 
1166         abstract fun setValue(view: View, value: Int)
1167 
1168         abstract fun getValue(view: View): Int
1169     }
1170 
1171     /** Simple data class to hold a set of dimens for left, top, right, bottom. */
1172     private data class DimenHolder(val left: Int, val top: Int, val right: Int, val bottom: Int)
1173 }
1174