diff options
3 files changed, 381 insertions, 64 deletions
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 1b7e26b0aea0..58ffef25cb42 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 @@ class ViewHierarchyAnimator { * [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 @@ class ViewHierarchyAnimator { 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 @@ class ViewHierarchyAnimator { val endValues = processEndValuesForRemoval( destination, + rootView, rootView.left, rootView.top, rootView.right, - rootView.bottom + rootView.bottom, + includeMargins, ) val boundsToAnimate = mutableSetOf<Bound>() @@ -718,70 +723,111 @@ class ViewHierarchyAnimator { * | | -> | | -> | | -> 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 @@ class ViewHierarchyAnimator { 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 5d631450cb41..ca066f472c0c 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 @@ open class MediaTttChipControllerSender @Inject constructor( 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 986e7cdbf6ad..6ab54a374d30 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 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { 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 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { 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 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val secondChild = View(mContext) rootView.addView(secondChild) - val childParams = LinearLayout.LayoutParams( + val firstChildParams = LinearLayout.LayoutParams( + 0 /* width */, + LinearLayout.LayoutParams.MATCH_PARENT + ) + 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 ) - childParams.weight = 0.5f - firstChild.layoutParams = childParams - secondChild.layoutParams = childParams + 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 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } } } + +// 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 |