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