[Media TTT] Add the margins as part of the removal animation.

Bug: 203800644
Test: manual: Remove chip and verify that the chip also moves up
during the animation (see video attached to bug)

Change-Id: I7ce7516ee1ba3c92f5f8c7e1f3ef8bce6a0f5f6d
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index 1b7e26b..58ffef2 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -360,7 +360,9 @@
          * [interpolator] and [duration].
          *
          * The end state of the animation is controlled by [destination]. This value can be any of
-         * the four corners, any of the four edges, or the center of the view.
+         * the four corners, any of the four edges, or the center of the view. If any margins are
+         * added on the side(s) of the [destination], the translation of those margins can be
+         * included by specifying [includeMargins].
          *
          * @param onAnimationEnd an optional runnable that will be run once the animation finishes
          *    successfully. Will not be run if the animation is cancelled.
@@ -371,6 +373,7 @@
             destination: Hotspot = Hotspot.CENTER,
             interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
             duration: Long = DEFAULT_DURATION,
+            includeMargins: Boolean = false,
             onAnimationEnd: Runnable? = null,
         ): Boolean {
             if (
@@ -428,10 +431,12 @@
             val endValues =
                 processEndValuesForRemoval(
                     destination,
+                    rootView,
                     rootView.left,
                     rootView.top,
                     rootView.right,
-                    rootView.bottom
+                    rootView.bottom,
+                    includeMargins,
                 )
 
             val boundsToAnimate = mutableSetOf<Bound>()
@@ -718,70 +723,111 @@
          *         |         | ->  |       |  ->   |     |   ->    x---x    ->      x
          *         |         |     x-------x       x-----x
          *         x---------x
+         *     4) destination=TOP, includeMargins=true (and view has large top margin)
+         *                                                                     x---------x
+         *                                                      x---------x
+         *                                       x---------x    x---------x
+         *                        x---------x    |         |
+         *         x---------x    |         |    x---------x
+         *         |         |    |         |
+         *         |         | -> x---------x ->             ->             ->
+         *         |         |
+         *         x---------x
          * ```
          */
         private fun processEndValuesForRemoval(
             destination: Hotspot,
+            rootView: View,
             left: Int,
             top: Int,
             right: Int,
-            bottom: Int
+            bottom: Int,
+            includeMargins: Boolean = false,
         ): Map<Bound, Int> {
-            val endLeft =
-                when (destination) {
-                    Hotspot.CENTER -> (left + right) / 2
-                    Hotspot.BOTTOM,
-                    Hotspot.BOTTOM_LEFT,
-                    Hotspot.LEFT,
-                    Hotspot.TOP_LEFT,
-                    Hotspot.TOP -> left
-                    Hotspot.TOP_RIGHT,
-                    Hotspot.RIGHT,
-                    Hotspot.BOTTOM_RIGHT -> right
-                }
-            val endTop =
-                when (destination) {
-                    Hotspot.CENTER -> (top + bottom) / 2
-                    Hotspot.LEFT,
-                    Hotspot.TOP_LEFT,
-                    Hotspot.TOP,
-                    Hotspot.TOP_RIGHT,
-                    Hotspot.RIGHT -> top
-                    Hotspot.BOTTOM_RIGHT,
-                    Hotspot.BOTTOM,
-                    Hotspot.BOTTOM_LEFT -> bottom
-                }
-            val endRight =
-                when (destination) {
-                    Hotspot.CENTER -> (left + right) / 2
-                    Hotspot.TOP,
-                    Hotspot.TOP_RIGHT,
-                    Hotspot.RIGHT,
-                    Hotspot.BOTTOM_RIGHT,
-                    Hotspot.BOTTOM -> right
-                    Hotspot.BOTTOM_LEFT,
-                    Hotspot.LEFT,
-                    Hotspot.TOP_LEFT -> left
-                }
-            val endBottom =
-                when (destination) {
-                    Hotspot.CENTER -> (top + bottom) / 2
-                    Hotspot.RIGHT,
-                    Hotspot.BOTTOM_RIGHT,
-                    Hotspot.BOTTOM,
-                    Hotspot.BOTTOM_LEFT,
-                    Hotspot.LEFT -> bottom
-                    Hotspot.TOP_LEFT,
-                    Hotspot.TOP,
-                    Hotspot.TOP_RIGHT -> top
-                }
+            val marginAdjustment =
+                if (includeMargins &&
+                    (rootView.layoutParams is ViewGroup.MarginLayoutParams)) {
+                    val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams
+                    DimenHolder(
+                        left = marginLp.leftMargin,
+                        top = marginLp.topMargin,
+                        right = marginLp.rightMargin,
+                        bottom = marginLp.bottomMargin
+                    )
+            } else {
+                DimenHolder(0, 0, 0, 0)
+            }
 
-            return mapOf(
-                Bound.LEFT to endLeft,
-                Bound.TOP to endTop,
-                Bound.RIGHT to endRight,
-                Bound.BOTTOM to endBottom
-            )
+            // These are the end values to use *if* this bound is part of the destination.
+            val endLeft = left - marginAdjustment.left
+            val endTop = top - marginAdjustment.top
+            val endRight = right + marginAdjustment.right
+            val endBottom = bottom + marginAdjustment.bottom
+
+            // For the below calculations: We need to ensure that the destination bound and the
+            // bound *opposite* to the destination bound end at the same value, to ensure that the
+            // view has size 0 for that dimension.
+            // For example,
+            //  - If destination=TOP, then endTop == endBottom. Left and right stay the same.
+            //  - If destination=RIGHT, then endRight == endLeft. Top and bottom stay the same.
+            //  - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight.
+
+            return when (destination) {
+                Hotspot.TOP -> mapOf(
+                    Bound.TOP to endTop,
+                    Bound.BOTTOM to endTop,
+                    Bound.LEFT to left,
+                    Bound.RIGHT to right,
+                )
+                Hotspot.TOP_RIGHT -> mapOf(
+                    Bound.TOP to endTop,
+                    Bound.BOTTOM to endTop,
+                    Bound.RIGHT to endRight,
+                    Bound.LEFT to endRight,
+                )
+                Hotspot.RIGHT -> mapOf(
+                    Bound.RIGHT to endRight,
+                    Bound.LEFT to endRight,
+                    Bound.TOP to top,
+                    Bound.BOTTOM to bottom,
+                )
+                Hotspot.BOTTOM_RIGHT -> mapOf(
+                    Bound.BOTTOM to endBottom,
+                    Bound.TOP to endBottom,
+                    Bound.RIGHT to endRight,
+                    Bound.LEFT to endRight,
+                )
+                Hotspot.BOTTOM -> mapOf(
+                    Bound.BOTTOM to endBottom,
+                    Bound.TOP to endBottom,
+                    Bound.LEFT to left,
+                    Bound.RIGHT to right,
+                )
+                Hotspot.BOTTOM_LEFT -> mapOf(
+                    Bound.BOTTOM to endBottom,
+                    Bound.TOP to endBottom,
+                    Bound.LEFT to endLeft,
+                    Bound.RIGHT to endLeft,
+                )
+                Hotspot.LEFT -> mapOf(
+                    Bound.LEFT to endLeft,
+                    Bound.RIGHT to endLeft,
+                    Bound.TOP to top,
+                    Bound.BOTTOM to bottom,
+                )
+                Hotspot.TOP_LEFT -> mapOf(
+                    Bound.TOP to endTop,
+                    Bound.BOTTOM to endTop,
+                    Bound.LEFT to endLeft,
+                    Bound.RIGHT to endLeft,
+                )
+                Hotspot.CENTER -> mapOf(
+                    Bound.LEFT to (endLeft + endRight) / 2,
+                    Bound.RIGHT to (endLeft + endRight) / 2,
+                    Bound.TOP to (endTop + endBottom) / 2,
+                    Bound.BOTTOM to (endTop + endBottom) / 2,
+                )
+            }
         }
 
         /**
@@ -1061,4 +1107,12 @@
         abstract fun setValue(view: View, value: Int)
         abstract fun getValue(view: View): Int
     }
+
+    /** Simple data class to hold a set of dimens for left, top, right, bottom. */
+    private data class DimenHolder(
+        val left: Int,
+        val top: Int,
+        val right: Int,
+        val bottom: Int,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 5d63145..ca066f4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -199,10 +199,9 @@
             ViewHierarchyAnimator.Hotspot.TOP,
             Interpolators.EMPHASIZED_ACCELERATE,
             ANIMATION_DURATION,
+            includeMargins = true,
             onAnimationEnd,
         )
-        // TODO(b/203800644): Add includeMargins as an option to ViewHierarchyAnimator so that the
-        //   animateChipOut matches the animateChipIn.
     }
 
     override fun shouldIgnoreViewRemoval(info: ChipSenderInfo, removalReason: String): Boolean {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
index 986e7cd..6ab54a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -935,6 +935,251 @@
         checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
     }
 
+    /* ******** start of animatesViewRemoval_includeMarginsTrue tests ******** */
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_center() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalLeft = removedChild.left
+        val originalTop = removedChild.top
+        val originalRight = removedChild.right
+        val originalBottom = removedChild.bottom
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.CENTER,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        val expectedX = ((originalLeft - M_LEFT) + (originalRight + M_RIGHT)) / 2
+        val expectedY = ((originalTop - M_TOP) + (originalBottom + M_BOTTOM)) / 2
+
+        checkBounds(
+            removedChild,
+            l = expectedX,
+            t = expectedY,
+            r = expectedX,
+            b = expectedY
+        )
+    }
+
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_left() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalLeft = removedChild.left
+        val originalTop = removedChild.top
+        val originalBottom = removedChild.bottom
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.LEFT,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        checkBounds(
+            removedChild,
+            l = originalLeft - M_LEFT,
+            t = originalTop,
+            r = originalLeft - M_LEFT,
+            b = originalBottom
+        )
+    }
+
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_topLeft() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalLeft = removedChild.left
+        val originalTop = removedChild.top
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        checkBounds(
+            removedChild,
+            l = originalLeft - M_LEFT,
+            t = originalTop - M_TOP,
+            r = originalLeft - M_LEFT,
+            b = originalTop - M_TOP
+        )
+    }
+
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_top() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalLeft = removedChild.left
+        val originalTop = removedChild.top
+        val originalRight = removedChild.right
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.TOP,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        checkBounds(
+            removedChild,
+            l = originalLeft,
+            t = originalTop - M_TOP,
+            r = originalRight,
+            b = originalTop - M_TOP
+        )
+    }
+
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_topRight() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalTop = removedChild.top
+        val originalRight = removedChild.right
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        checkBounds(
+            removedChild,
+            l = originalRight + M_RIGHT,
+            t = originalTop - M_TOP,
+            r = originalRight + M_RIGHT,
+            b = originalTop - M_TOP
+        )
+    }
+
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_right() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalTop = removedChild.top
+        val originalRight = removedChild.right
+        val originalBottom = removedChild.bottom
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.RIGHT,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        checkBounds(
+            removedChild,
+            l = originalRight + M_RIGHT,
+            t = originalTop,
+            r = originalRight + M_RIGHT,
+            b = originalBottom
+        )
+    }
+
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_bottomRight() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalRight = removedChild.right
+        val originalBottom = removedChild.bottom
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        checkBounds(
+            removedChild,
+            l = originalRight + M_RIGHT,
+            t = originalBottom + M_BOTTOM,
+            r = originalRight + M_RIGHT,
+            b = originalBottom + M_BOTTOM
+        )
+    }
+
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_bottom() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalLeft = removedChild.left
+        val originalRight = removedChild.right
+        val originalBottom = removedChild.bottom
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.BOTTOM,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        checkBounds(
+            removedChild,
+            l = originalLeft,
+            t = originalBottom + M_BOTTOM,
+            r = originalRight,
+            b = originalBottom + M_BOTTOM
+        )
+    }
+
+    @Test
+    fun animatesViewRemoval_includeMarginsTrue_bottomLeft() {
+        setUpRootWithChildren(includeMarginsOnFirstChild = true)
+        val removedChild = rootView.getChildAt(0)
+        val originalLeft = removedChild.left
+        val originalBottom = removedChild.bottom
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            removedChild,
+            destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
+            includeMargins = true,
+        )
+        forceLayout()
+
+        assertTrue(success)
+        assertNotNull(removedChild.getTag(R.id.tag_animator))
+        advanceAnimation(removedChild, 1.0f)
+        checkBounds(
+            removedChild,
+            l = originalLeft - M_LEFT,
+            t = originalBottom + M_BOTTOM,
+            r = originalLeft - M_LEFT,
+            b = originalBottom + M_BOTTOM
+        )
+    }
+    /* ******** end of animatesViewRemoval_includeMarginsTrue tests ******** */
+
     @Test
     fun animatesChildrenDuringViewRemoval() {
         setUpRootWithChildren()
@@ -1215,7 +1460,7 @@
         checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
     }
 
-    private fun setUpRootWithChildren() {
+    private fun setUpRootWithChildren(includeMarginsOnFirstChild: Boolean = false) {
         rootView = LinearLayout(mContext)
         (rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL
         (rootView as LinearLayout).weightSum = 1f
@@ -1229,13 +1474,26 @@
         val secondChild = View(mContext)
         rootView.addView(secondChild)
 
-        val childParams = LinearLayout.LayoutParams(
+        val firstChildParams = LinearLayout.LayoutParams(
             0 /* width */,
             LinearLayout.LayoutParams.MATCH_PARENT
         )
-        childParams.weight = 0.5f
-        firstChild.layoutParams = childParams
-        secondChild.layoutParams = childParams
+        firstChildParams.weight = 0.5f
+        if (includeMarginsOnFirstChild) {
+            firstChildParams.leftMargin = M_LEFT
+            firstChildParams.topMargin = M_TOP
+            firstChildParams.rightMargin = M_RIGHT
+            firstChildParams.bottomMargin = M_BOTTOM
+        }
+        firstChild.layoutParams = firstChildParams
+
+        val secondChildParams = LinearLayout.LayoutParams(
+            0 /* width */,
+            LinearLayout.LayoutParams.MATCH_PARENT
+        )
+        secondChildParams.weight = 0.5f
+        secondChild.layoutParams = secondChildParams
+
         firstGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
         (firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
             .addRule(RelativeLayout.ALIGN_PARENT_START)
@@ -1315,3 +1573,9 @@
         }
     }
 }
+
+// Margin values.
+private const val M_LEFT = 14
+private const val M_TOP = 16
+private const val M_RIGHT = 18
+private const val M_BOTTOM = 20