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