Merge "Add animateRemoval() to the ViewBoundAnimator API." into tm-dev am: f2bbd4e746 am: b1e3f5909b

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17449405

Change-Id: I3c3b7d5ac117e57c58cc3a847011b1033e4bea78
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Luca Zuccarini
2022-04-28 09:43:14 +00:00
committed by Automerger Merge Worker
2 changed files with 634 additions and 34 deletions

View File

@@ -20,6 +20,7 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.animation.ValueAnimator
import android.util.IntProperty
import android.view.View
import android.view.ViewGroup
@@ -37,6 +38,7 @@ class ViewHierarchyAnimator {
private const val DEFAULT_DURATION = 500L
private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD
private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE
/** The properties used to animate the view bounds. */
private val PROPERTIES = mapOf(
@@ -113,7 +115,7 @@ class ViewHierarchyAnimator {
}
val listener = createUpdateListener(interpolator, duration, ephemeral)
recursivelyAddListener(rootView, listener)
addListener(rootView, listener, recursive = true)
return true
}
@@ -183,7 +185,7 @@ class ViewHierarchyAnimator {
val listener = createAdditionListener(
origin, interpolator, duration, ignorePreviousValues = !includeMargins
)
recursivelyAddListener(rootView, listener)
addListener(rootView, listener, recursive = true)
return true
}
@@ -297,6 +299,183 @@ class ViewHierarchyAnimator {
}
}
/**
* Animates the removal of [rootView] and its children from the hierarchy. It uses the given
* [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.
*/
@JvmOverloads
fun animateRemoval(
rootView: View,
destination: Hotspot = Hotspot.CENTER,
interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
duration: Long = DEFAULT_DURATION
): Boolean {
if (!isVisible(
rootView.visibility,
rootView.left,
rootView.top,
rootView.right,
rootView.bottom
)
) {
return false
}
val parent = rootView.parent as ViewGroup
// Ensure that rootView's siblings animate nicely around the removal.
val listener = createUpdateListener(
interpolator,
duration,
ephemeral = true
)
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child == rootView) continue
addListener(child, listener, recursive = false)
}
// Remove the view so that a layout update is triggered for the siblings and they
// animate to their next position while the view's removal is also animating.
parent.removeView(rootView)
// By adding the view to the overlay, we can animate it while it isn't part of the view
// hierarchy. It is correctly positioned because we have its previous bounds, and we set
// them manually during the animation.
parent.overlay.add(rootView)
val startValues = mapOf(
Bound.LEFT to rootView.left,
Bound.TOP to rootView.top,
Bound.RIGHT to rootView.right,
Bound.BOTTOM to rootView.bottom
)
val endValues = processEndValuesForRemoval(
destination,
rootView.left,
rootView.top,
rootView.right,
rootView.bottom
)
val boundsToAnimate = mutableSetOf<Bound>()
if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT)
if (rootView.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP)
if (rootView.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT)
if (rootView.bottom != endValues.getValue(Bound.BOTTOM)) {
boundsToAnimate.add(Bound.BOTTOM)
}
startAnimation(
rootView,
boundsToAnimate,
startValues,
endValues,
interpolator,
duration,
ephemeral = true
)
if (rootView is ViewGroup) {
// Shift the children so they maintain a consistent position within the shrinking
// view.
shiftChildrenForRemoval(rootView, destination, endValues, interpolator, duration)
// Fade out the children during the first half of the removal, so they don't clutter
// too much once the view becomes very small. Then we fade out the view itself, in
// case it has its own content and/or background.
val startAlphas = FloatArray(rootView.childCount)
for (i in 0 until rootView.childCount) {
startAlphas[i] = rootView.getChildAt(i).alpha
}
val animator = ValueAnimator.ofFloat(1f, 0f)
animator.interpolator = Interpolators.ALPHA_OUT
animator.duration = duration / 2
animator.addUpdateListener { animation ->
for (i in 0 until rootView.childCount) {
rootView.getChildAt(i).alpha =
(animation.animatedValue as Float) * startAlphas[i]
}
}
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
rootView.animate()
.alpha(0f)
.setInterpolator(Interpolators.ALPHA_OUT)
.setDuration(duration / 2)
.withEndAction { parent.overlay.remove(rootView) }
.start()
}
})
animator.start()
} else {
// Fade out the view during the second half of the removal.
rootView.animate()
.alpha(0f)
.setInterpolator(Interpolators.ALPHA_OUT)
.setDuration(duration / 2)
.setStartDelay(duration / 2)
.withEndAction { parent.overlay.remove(rootView) }
.start()
}
return true
}
/**
* Animates the children of [rootView] so that its layout remains internally consistent as
* it shrinks towards [destination] and changes its bounds to [endValues].
*
* Uses [interpolator] and [duration], which should match those of the removal animation.
*/
private fun shiftChildrenForRemoval(
rootView: ViewGroup,
destination: Hotspot,
endValues: Map<Bound, Int>,
interpolator: Interpolator,
duration: Long
) {
for (i in 0 until rootView.childCount) {
val child = rootView.getChildAt(i)
val childStartValues = mapOf(
Bound.LEFT to child.left,
Bound.TOP to child.top,
Bound.RIGHT to child.right,
Bound.BOTTOM to child.bottom
)
val childEndValues = processChildEndValuesForRemoval(
destination,
child.left,
child.top,
child.right,
child.bottom,
endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT),
endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP)
)
val boundsToAnimate = mutableSetOf<Bound>()
if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT)
if (child.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP)
if (child.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT)
if (child.bottom != endValues.getValue(Bound.BOTTOM)) {
boundsToAnimate.add(Bound.BOTTOM)
}
startAnimation(
child,
boundsToAnimate,
childStartValues,
childEndValues,
interpolator,
duration,
ephemeral = true
)
}
}
/**
* Returns whether the given [visibility] and bounds are consistent with a view being
* currently visible on screen.
@@ -312,7 +491,7 @@ class ViewHierarchyAnimator {
}
/**
* Compute the actual starting values based on the requested [origin] and on
* Computes the actual starting values based on the requested [origin] and on
* [ignorePreviousValues].
*
* If [origin] is null, the resolved start values will be the same as those passed in, or
@@ -422,7 +601,140 @@ class ViewHierarchyAnimator {
)
}
private fun recursivelyAddListener(view: View, listener: View.OnLayoutChangeListener) {
/**
* Computes a removal animation's end values based on the requested [destination] and the
* view's starting bounds.
*
* Examples:
* 1) destination=TOP
* x---------x x---------x x---------x x---------x x---------x
* | | | | | | x---------x
* | | -> | | -> x---------x -> ->
* | | x---------x
* x---------x
* 2) destination=BOTTOM_LEFT
* x---------x
* | | x-------x
* | | -> | | -> x----x -> ->
* | | | | | | x--x
* x---------x x-------x x----x x--x x
* 3) destination=CENTER
* x---------x
* | | x-------x x-----x
* | | -> | | -> | | -> x---x -> x
* | | x-------x x-----x
* x---------x
*/
private fun processEndValuesForRemoval(
destination: Hotspot,
left: Int,
top: Int,
right: Int,
bottom: Int
): 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
}
return mapOf(
Bound.LEFT to endLeft,
Bound.TOP to endTop,
Bound.RIGHT to endRight,
Bound.BOTTOM to endBottom
)
}
/**
* Computes the end values for the child of a view being removed, based on the child's
* starting bounds, the removal's [destination], and the [parentWidth] and [parentHeight].
*
* The end values always represent the child's position after it has been translated so that
* its center is at the [destination].
*
* Examples:
* 1) destination=TOP
* The child maintains its left and right positions, but is shifted up so that its
* center is on the parent's end top edge.
* 2) destination=BOTTOM_LEFT
* The child shifts so that its center is on the parent's end bottom left corner.
* 3) destination=CENTER
* The child shifts so that its own center is on the parent's end center.
*/
private fun processChildEndValuesForRemoval(
destination: Hotspot,
left: Int,
top: Int,
right: Int,
bottom: Int,
parentWidth: Int,
parentHeight: Int
): Map<Bound, Int> {
val halfWidth = (right - left) / 2
val halfHeight = (bottom - top) / 2
val endLeft = when (destination) {
Hotspot.CENTER -> (parentWidth / 2) - halfWidth
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> -halfWidth
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth
Hotspot.TOP, Hotspot.BOTTOM -> left
}
val endTop = when (destination) {
Hotspot.CENTER -> (parentHeight / 2) - halfHeight
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> -halfHeight
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT ->
parentHeight - halfHeight
Hotspot.LEFT, Hotspot.RIGHT -> top
}
val endRight = when (destination) {
Hotspot.CENTER -> (parentWidth / 2) + halfWidth
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> halfWidth
Hotspot.TOP, Hotspot.BOTTOM -> right
}
val endBottom = when (destination) {
Hotspot.CENTER -> (parentHeight / 2) + halfHeight
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT ->
parentHeight + halfHeight
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> halfHeight
Hotspot.LEFT, Hotspot.RIGHT -> bottom
}
return mapOf(
Bound.LEFT to endLeft,
Bound.TOP to endTop,
Bound.RIGHT to endRight,
Bound.BOTTOM to endBottom
)
}
private fun addListener(
view: View,
listener: View.OnLayoutChangeListener,
recursive: Boolean = false
) {
// Make sure that only one listener is active at a time.
val previousListener = view.getTag(R.id.tag_layout_listener)
if (previousListener != null && previousListener is View.OnLayoutChangeListener) {
@@ -431,9 +743,9 @@ class ViewHierarchyAnimator {
view.addOnLayoutChangeListener(listener)
view.setTag(R.id.tag_layout_listener, listener)
if (view is ViewGroup) {
if (view is ViewGroup && recursive) {
for (i in 0 until view.childCount) {
recursivelyAddListener(view.getChildAt(i), listener)
addListener(view.getChildAt(i), listener, recursive = true)
}
}
}
@@ -490,6 +802,8 @@ class ViewHierarchyAnimator {
}
}.toTypedArray()
(view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
animator.interpolator = interpolator
animator.duration = duration

View File

@@ -6,8 +6,10 @@ import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.children
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNotNull
@@ -28,18 +30,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
private val TEST_INTERPOLATOR = Interpolators.LINEAR
}
private val childParams = LinearLayout.LayoutParams(
0 /* width */,
LinearLayout.LayoutParams.MATCH_PARENT
)
private lateinit var rootView: LinearLayout
private lateinit var rootView: ViewGroup
@Before
fun setUp() {
rootView = LinearLayout(mContext)
rootView.orientation = LinearLayout.HORIZONTAL
rootView.weightSum = 1f
childParams.weight = 0.5f
}
@After
@@ -93,6 +88,19 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
assertEquals(animator.interpolator, TEST_INTERPOLATOR)
assertEquals(animator.duration, TEST_DURATION)
// animateRemoval()
setUpRootWithChildren()
val child = rootView.getChildAt(0)
success = ViewHierarchyAnimator.animateRemoval(
child, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
)
assertTrue(success)
assertNotNull(child.getTag(R.id.tag_animator))
animator = child.getTag(R.id.tag_animator) as ObjectAnimator
assertEquals(animator.interpolator, TEST_INTERPOLATOR)
assertEquals(animator.duration, TEST_DURATION)
}
@Test
@@ -170,17 +178,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
@Test
fun animatesRootAndChildren() {
val firstChild = View(mContext)
firstChild.layoutParams = childParams
rootView.addView(firstChild)
val secondChild = View(mContext)
secondChild.layoutParams = childParams
rootView.addView(secondChild)
rootView.measure(
View.MeasureSpec.makeMeasureSpec(150, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
)
rootView.layout(0 /* l */, 0 /* t */, 150 /* r */, 100 /* b */)
setUpRootWithChildren()
val success = ViewHierarchyAnimator.animate(rootView)
// Change all bounds.
@@ -192,20 +190,20 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
assertNotNull(firstChild.getTag(R.id.tag_animator))
assertNotNull(secondChild.getTag(R.id.tag_animator))
assertNotNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
// The initial values should be those of the previous layout.
checkBounds(rootView, l = 0, t = 0, r = 150, b = 100)
checkBounds(firstChild, l = 0, t = 0, r = 75, b = 100)
checkBounds(secondChild, l = 75, t = 0, r = 150, b = 100)
checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 100, b = 100)
checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100)
endAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_animator))
assertNull(firstChild.getTag(R.id.tag_animator))
assertNull(secondChild.getTag(R.id.tag_animator))
assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
// The end values should be those of the latest layout.
checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
checkBounds(firstChild, l = 0, t = 0, r = 95, b = 100)
checkBounds(secondChild, l = 95, t = 0, r = 190, b = 100)
checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 95, b = 100)
checkBounds(rootView.getChildAt(1), l = 95, t = 0, r = 190, b = 100)
}
@Test
@@ -522,6 +520,251 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
endAnimation(rootView)
}
fun animatesViewRemovalFromStartToEnd() {
setUpRootWithChildren()
val child = rootView.getChildAt(0)
val success = ViewHierarchyAnimator.animateRemoval(
child,
destination = ViewHierarchyAnimator.Hotspot.LEFT,
interpolator = Interpolators.LINEAR
)
assertTrue(success)
assertNotNull(child.getTag(R.id.tag_animator))
checkBounds(child, l = 0, t = 0, r = 100, b = 100)
advanceAnimation(child, 0.5f)
checkBounds(child, l = 0, t = 0, r = 50, b = 100)
advanceAnimation(child, 1.0f)
checkBounds(child, l = 0, t = 0, r = 0, b = 100)
endAnimation(rootView)
endAnimation(child)
assertEquals(1, rootView.childCount)
assertFalse(child in rootView.children)
}
@Test
fun animatesViewRemovalRespectingDestination() {
// CENTER
setUpRootWithChildren()
var removedChild = rootView.getChildAt(0)
var remainingChild = rootView.getChildAt(1)
var success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.CENTER
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 50, t = 50, r = 50, b = 50)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
// LEFT
setUpRootWithChildren()
removedChild = rootView.getChildAt(0)
remainingChild = rootView.getChildAt(1)
success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.LEFT
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 0, t = 0, r = 0, b = 100)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
// TOP_LEFT
setUpRootWithChildren()
removedChild = rootView.getChildAt(0)
remainingChild = rootView.getChildAt(1)
success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 0, t = 0, r = 0, b = 0)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
// TOP
setUpRootWithChildren()
removedChild = rootView.getChildAt(0)
remainingChild = rootView.getChildAt(1)
success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 0, t = 0, r = 100, b = 0)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
// TOP_RIGHT
setUpRootWithChildren()
removedChild = rootView.getChildAt(0)
remainingChild = rootView.getChildAt(1)
success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 100, t = 0, r = 100, b = 0)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
// RIGHT
setUpRootWithChildren()
removedChild = rootView.getChildAt(0)
remainingChild = rootView.getChildAt(1)
success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.RIGHT
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 100, t = 0, r = 100, b = 100)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
// BOTTOM_RIGHT
setUpRootWithChildren()
removedChild = rootView.getChildAt(0)
remainingChild = rootView.getChildAt(1)
success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 100, t = 100, r = 100, b = 100)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
// BOTTOM
setUpRootWithChildren()
removedChild = rootView.getChildAt(0)
remainingChild = rootView.getChildAt(1)
success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 0, t = 100, r = 100, b = 100)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
// BOTTOM_LEFT
setUpRootWithChildren()
removedChild = rootView.getChildAt(0)
remainingChild = rootView.getChildAt(1)
success = ViewHierarchyAnimator.animateRemoval(
removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(removedChild.getTag(R.id.tag_animator))
advanceAnimation(removedChild, 1.0f)
checkBounds(removedChild, l = 0, t = 100, r = 0, b = 100)
endAnimation(rootView)
endAnimation(removedChild)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
}
@Test
fun animatesChildrenDuringViewRemoval() {
setUpRootWithChildren()
val child = rootView.getChildAt(0) as ViewGroup
val firstGrandChild = child.getChildAt(0)
val secondGrandChild = child.getChildAt(1)
val success = ViewHierarchyAnimator.animateRemoval(
child, interpolator = Interpolators.LINEAR
)
assertTrue(success)
assertNotNull(child.getTag(R.id.tag_animator))
assertNotNull(firstGrandChild.getTag(R.id.tag_animator))
assertNotNull(secondGrandChild.getTag(R.id.tag_animator))
checkBounds(child, l = 0, t = 0, r = 100, b = 100)
checkBounds(firstGrandChild, l = 0, t = 0, r = 40, b = 40)
checkBounds(secondGrandChild, l = 60, t = 60, r = 100, b = 100)
advanceAnimation(child, 0.5f)
checkBounds(child, l = 25, t = 25, r = 75, b = 75)
checkBounds(firstGrandChild, l = -10, t = -10, r = 30, b = 30)
checkBounds(secondGrandChild, l = 20, t = 20, r = 60, b = 60)
advanceAnimation(child, 1.0f)
checkBounds(child, l = 50, t = 50, r = 50, b = 50)
checkBounds(firstGrandChild, l = -20, t = -20, r = 20, b = 20)
checkBounds(secondGrandChild, l = -20, t = -20, r = 20, b = 20)
endAnimation(rootView)
endAnimation(child)
}
@Test
fun animatesSiblingsDuringViewRemoval() {
setUpRootWithChildren()
val removedChild = rootView.getChildAt(0)
val remainingChild = rootView.getChildAt(1)
val success = ViewHierarchyAnimator.animateRemoval(
removedChild, interpolator = Interpolators.LINEAR
)
// Ensure that the layout happens before the checks.
forceLayout()
assertTrue(success)
assertNotNull(remainingChild.getTag(R.id.tag_animator))
checkBounds(remainingChild, l = 100, t = 0, r = 200, b = 100)
advanceAnimation(rootView, 0.5f)
checkBounds(remainingChild, l = 50, t = 0, r = 150, b = 100)
advanceAnimation(rootView, 1.0f)
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
endAnimation(rootView)
endAnimation(removedChild)
assertNull(remainingChild.getTag(R.id.tag_animator))
}
@Test
fun cleansUpListenersCorrectly() {
val firstChild = View(mContext)
@@ -700,6 +943,49 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
}
private fun setUpRootWithChildren() {
rootView = LinearLayout(mContext)
(rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL
(rootView as LinearLayout).weightSum = 1f
val firstChild = RelativeLayout(mContext)
rootView.addView(firstChild)
val firstGrandChild = View(mContext)
firstChild.addView(firstGrandChild)
val secondGrandChild = View(mContext)
firstChild.addView(secondGrandChild)
val secondChild = View(mContext)
rootView.addView(secondChild)
val childParams = LinearLayout.LayoutParams(
0 /* width */,
LinearLayout.LayoutParams.MATCH_PARENT
)
childParams.weight = 0.5f
firstChild.layoutParams = childParams
secondChild.layoutParams = childParams
firstGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
(firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
.addRule(RelativeLayout.ALIGN_PARENT_START)
(firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
.addRule(RelativeLayout.ALIGN_PARENT_TOP)
secondGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
(secondGrandChild.layoutParams as RelativeLayout.LayoutParams)
.addRule(RelativeLayout.ALIGN_PARENT_END)
(secondGrandChild.layoutParams as RelativeLayout.LayoutParams)
.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
forceLayout()
}
private fun forceLayout() {
rootView.measure(
View.MeasureSpec.makeMeasureSpec(200 /* width */, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(100 /* height */, View.MeasureSpec.AT_MOST)
)
rootView.layout(0 /* l */, 0 /* t */, 200 /* r */, 100 /* b */)
}
private fun checkBounds(v: View, l: Int, t: Int, r: Int, b: Int) {
assertEquals(l, v.left)
assertEquals(t, v.top)