[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
This commit is contained in:
Caitlin Shkuratov
2022-10-07 16:44:47 +00:00
parent 693d0a4131
commit 6d7f8e203c
3 changed files with 381 additions and 64 deletions

View File

@@ -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,
)
}

View File

@@ -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 {

View File

@@ -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
)
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 @@ 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