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