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

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

Change-Id: I2db77182edf7f197c3a4b3cf253b86f0fc114345
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:34:11 +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.AnimatorListenerAdapter
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder import android.animation.PropertyValuesHolder
import android.animation.ValueAnimator
import android.util.IntProperty import android.util.IntProperty
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -37,6 +38,7 @@ class ViewHierarchyAnimator {
private const val DEFAULT_DURATION = 500L private const val DEFAULT_DURATION = 500L
private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD
private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE
/** The properties used to animate the view bounds. */ /** The properties used to animate the view bounds. */
private val PROPERTIES = mapOf( private val PROPERTIES = mapOf(
@@ -113,7 +115,7 @@ class ViewHierarchyAnimator {
} }
val listener = createUpdateListener(interpolator, duration, ephemeral) val listener = createUpdateListener(interpolator, duration, ephemeral)
recursivelyAddListener(rootView, listener) addListener(rootView, listener, recursive = true)
return true return true
} }
@@ -183,7 +185,7 @@ class ViewHierarchyAnimator {
val listener = createAdditionListener( val listener = createAdditionListener(
origin, interpolator, duration, ignorePreviousValues = !includeMargins origin, interpolator, duration, ignorePreviousValues = !includeMargins
) )
recursivelyAddListener(rootView, listener) addListener(rootView, listener, recursive = true)
return 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 * Returns whether the given [visibility] and bounds are consistent with a view being
* currently visible on screen. * 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]. * [ignorePreviousValues].
* *
* If [origin] is null, the resolved start values will be the same as those passed in, or * 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. // Make sure that only one listener is active at a time.
val previousListener = view.getTag(R.id.tag_layout_listener) val previousListener = view.getTag(R.id.tag_layout_listener)
if (previousListener != null && previousListener is View.OnLayoutChangeListener) { if (previousListener != null && previousListener is View.OnLayoutChangeListener) {
@@ -431,9 +743,9 @@ class ViewHierarchyAnimator {
view.addOnLayoutChangeListener(listener) view.addOnLayoutChangeListener(listener)
view.setTag(R.id.tag_layout_listener, 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) { 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() }.toTypedArray()
(view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders) val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
animator.interpolator = interpolator animator.interpolator = interpolator
animator.duration = duration animator.duration = duration

View File

@@ -6,8 +6,10 @@ import android.testing.TestableLooper
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.util.children
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNotNull import junit.framework.Assert.assertNotNull
@@ -28,18 +30,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
private val TEST_INTERPOLATOR = Interpolators.LINEAR private val TEST_INTERPOLATOR = Interpolators.LINEAR
} }
private val childParams = LinearLayout.LayoutParams( private lateinit var rootView: ViewGroup
0 /* width */,
LinearLayout.LayoutParams.MATCH_PARENT
)
private lateinit var rootView: LinearLayout
@Before @Before
fun setUp() { fun setUp() {
rootView = LinearLayout(mContext) rootView = LinearLayout(mContext)
rootView.orientation = LinearLayout.HORIZONTAL
rootView.weightSum = 1f
childParams.weight = 0.5f
} }
@After @After
@@ -93,6 +88,19 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
assertEquals(animator.interpolator, TEST_INTERPOLATOR) assertEquals(animator.interpolator, TEST_INTERPOLATOR)
assertEquals(animator.duration, TEST_DURATION) 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 @Test
@@ -170,17 +178,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
@Test @Test
fun animatesRootAndChildren() { fun animatesRootAndChildren() {
val firstChild = View(mContext) setUpRootWithChildren()
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 */)
val success = ViewHierarchyAnimator.animate(rootView) val success = ViewHierarchyAnimator.animate(rootView)
// Change all bounds. // Change all bounds.
@@ -192,20 +190,20 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
assertTrue(success) assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator)) assertNotNull(rootView.getTag(R.id.tag_animator))
assertNotNull(firstChild.getTag(R.id.tag_animator)) assertNotNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
assertNotNull(secondChild.getTag(R.id.tag_animator)) assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
// The initial values should be those of the previous layout. // The initial values should be those of the previous layout.
checkBounds(rootView, l = 0, t = 0, r = 150, b = 100) checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
checkBounds(firstChild, l = 0, t = 0, r = 75, b = 100) checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 100, b = 100)
checkBounds(secondChild, l = 75, t = 0, r = 150, b = 100) checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100)
endAnimation(rootView) endAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_animator)) assertNull(rootView.getTag(R.id.tag_animator))
assertNull(firstChild.getTag(R.id.tag_animator)) assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
assertNull(secondChild.getTag(R.id.tag_animator)) assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
// The end values should be those of the latest layout. // The end values should be those of the latest layout.
checkBounds(rootView, l = 10, t = 20, r = 200, b = 120) checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
checkBounds(firstChild, l = 0, t = 0, r = 95, b = 100) checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 95, b = 100)
checkBounds(secondChild, l = 95, t = 0, r = 190, b = 100) checkBounds(rootView.getChildAt(1), l = 95, t = 0, r = 190, b = 100)
} }
@Test @Test
@@ -522,6 +520,251 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
endAnimation(rootView) 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 @Test
fun cleansUpListenersCorrectly() { fun cleansUpListenersCorrectly() {
val firstChild = View(mContext) val firstChild = View(mContext)
@@ -700,6 +943,49 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50) 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) { private fun checkBounds(v: View, l: Int, t: Int, r: Int, b: Int) {
assertEquals(l, v.left) assertEquals(l, v.left)
assertEquals(t, v.top) assertEquals(t, v.top)