summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Luca Zuccarini <acul@google.com> 2022-03-18 15:44:39 +0000
committer Luca Zuccarini <acul@google.com> 2022-03-30 14:43:52 +0000
commit3ccbe8c15ac7b6800ea90dffcd5386fcce58116e (patch)
tree3e8f43cb1f7ae75ac41159f6ff83bf732bae6707
parentce407722207b15fc4e3d1ba0001681c9e2c08599 (diff)
Add animateAddition() to the ViewBoundAnimator API.
The goal is to have a clear API difference between views animating from not being on screen to being visible and the normal view bound animations. Allows the user to specify an origin to the addition animation, and animates the view starting from that position. Bug: 224980562 Test: manual and unit tests included Change-Id: If0e86a3d240582c3e276f76c029cb8d3251e82b0
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt407
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt482
2 files changed, 792 insertions, 97 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 c480197d23dc..d15b8c169535 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -24,59 +24,19 @@ import android.util.IntProperty
import android.view.View
import android.view.ViewGroup
import android.view.animation.Interpolator
+import kotlin.math.max
+import kotlin.math.min
/**
* A class that allows changes in bounds within a view hierarchy to animate seamlessly between the
* start and end state.
*/
class ViewHierarchyAnimator {
- // TODO(b/221418522): make this private once it can't be passed as an arg anymore.
- enum class Bound(val label: String, val overrideTag: Int) {
- LEFT("left", R.id.tag_override_left) {
- override fun setValue(view: View, value: Int) {
- view.left = value
- }
-
- override fun getValue(view: View): Int {
- return view.left
- }
- },
- TOP("top", R.id.tag_override_top) {
- override fun setValue(view: View, value: Int) {
- view.top = value
- }
-
- override fun getValue(view: View): Int {
- return view.top
- }
- },
- RIGHT("right", R.id.tag_override_right) {
- override fun setValue(view: View, value: Int) {
- view.right = value
- }
-
- override fun getValue(view: View): Int {
- return view.right
- }
- },
- BOTTOM("bottom", R.id.tag_override_bottom) {
- override fun setValue(view: View, value: Int) {
- view.bottom = value
- }
-
- override fun getValue(view: View): Int {
- return view.bottom
- }
- };
-
- abstract fun setValue(view: View, value: Int)
- abstract fun getValue(view: View): Int
- }
-
companion object {
/** Default values for the animation. These can all be overridden at call time. */
private const val DEFAULT_DURATION = 500L
- private val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED
+ private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD
+ private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
private val DEFAULT_BOUNDS = setOf(Bound.LEFT, Bound.TOP, Bound.RIGHT, Bound.BOTTOM)
/** The properties used to animate the view bounds. */
@@ -112,6 +72,9 @@ class ViewHierarchyAnimator {
* Successive calls to this method override the previous settings ([interpolator] and
* [duration]). The changes take effect on the next animation.
*
+ * Returns true if the [rootView] is already visible and will be animated, false otherwise.
+ * To animate the addition of a view, see [animateAddition].
+ *
* TODO(b/221418522): remove the ability to select which bounds to animate and always
* animate all of them.
*/
@@ -121,8 +84,8 @@ class ViewHierarchyAnimator {
bounds: Set<Bound> = DEFAULT_BOUNDS,
interpolator: Interpolator = DEFAULT_INTERPOLATOR,
duration: Long = DEFAULT_DURATION
- ) {
- animate(rootView, bounds, interpolator, duration, false /* ephemeral */)
+ ): Boolean {
+ return animate(rootView, bounds, interpolator, duration, ephemeral = false)
}
/**
@@ -138,8 +101,8 @@ class ViewHierarchyAnimator {
bounds: Set<Bound> = DEFAULT_BOUNDS,
interpolator: Interpolator = DEFAULT_INTERPOLATOR,
duration: Long = DEFAULT_DURATION
- ) {
- animate(rootView, bounds, interpolator, duration, true /* ephemeral */)
+ ): Boolean {
+ return animate(rootView, bounds, interpolator, duration, ephemeral = true)
}
private fun animate(
@@ -148,9 +111,42 @@ class ViewHierarchyAnimator {
interpolator: Interpolator,
duration: Long,
ephemeral: Boolean
- ) {
- val listener = createListener(bounds, interpolator, duration, ephemeral)
+ ): Boolean {
+ if (!isVisible(
+ rootView.visibility,
+ rootView.left,
+ rootView.top,
+ rootView.right,
+ rootView.bottom
+ )
+ ) {
+ return false
+ }
+
+ val listener = createUpdateListener(bounds, interpolator, duration, ephemeral)
recursivelyAddListener(rootView, listener)
+ return true
+ }
+
+ /**
+ * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation
+ * for the specified [bounds], using [interpolator] and [duration].
+ *
+ * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise
+ * it keeps listening for further updates.
+ */
+ private fun createUpdateListener(
+ bounds: Set<Bound>,
+ interpolator: Interpolator,
+ duration: Long,
+ ephemeral: Boolean
+ ): View.OnLayoutChangeListener {
+ return createListener(
+ bounds,
+ interpolator,
+ duration,
+ ephemeral
+ )
}
/**
@@ -173,11 +169,87 @@ class ViewHierarchyAnimator {
}
}
+ /**
+ * Instruct the animator to watch for changes to the layout of [rootView] and its children,
+ * and animate the next time the hierarchy appears after not being visible. It uses the
+ * given [interpolator] and [duration].
+ *
+ * The start state of the animation is controlled by [origin]. This value can be any of 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 origin, the translation of those margins can be included by
+ * specifying [includeMargins].
+ *
+ * Returns true if the [rootView] is invisible and will be animated, false otherwise. To
+ * animate an already visible view, see [animate] and [animateNextUpdate].
+ *
+ * Then animator unregisters itself once the first addition animation is complete.
+ */
+ @JvmOverloads
+ fun animateAddition(
+ rootView: View,
+ origin: Hotspot = Hotspot.CENTER,
+ interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR,
+ duration: Long = DEFAULT_DURATION,
+ includeMargins: Boolean = false
+ ): Boolean {
+ if (isVisible(
+ rootView.visibility,
+ rootView.left,
+ rootView.top,
+ rootView.right,
+ rootView.bottom
+ )
+ ) {
+ return false
+ }
+
+ val listener = createAdditionListener(
+ origin, interpolator, duration, ignorePreviousValues = !includeMargins
+ )
+ recursivelyAddListener(rootView, listener)
+ return true
+ }
+
+ /**
+ * Returns a new [View.OnLayoutChangeListener] that on the next call triggers a layout
+ * addition animation from the given [origin], using [interpolator] and [duration].
+ *
+ * If [ignorePreviousValues] is true, the animation will only span the area covered by the
+ * new bounds. Otherwise it will include the margins between the previous and new bounds.
+ */
+ private fun createAdditionListener(
+ origin: Hotspot,
+ interpolator: Interpolator,
+ duration: Long,
+ ignorePreviousValues: Boolean
+ ): View.OnLayoutChangeListener {
+ return createListener(
+ DEFAULT_BOUNDS,
+ interpolator,
+ duration,
+ ephemeral = true,
+ origin = origin,
+ ignorePreviousValues = ignorePreviousValues
+ )
+ }
+
+ /**
+ * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation
+ * for the specified [bounds], using [interpolator] and [duration].
+ *
+ * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise
+ * it keeps listening for further updates.
+ *
+ * [origin] specifies whether the start values should be determined by a hotspot, and
+ * [ignorePreviousValues] controls whether the previous values should be taken into account.
+ */
private fun createListener(
bounds: Set<Bound>,
interpolator: Interpolator,
duration: Long,
- ephemeral: Boolean
+ ephemeral: Boolean,
+ origin: Hotspot? = null,
+ ignorePreviousValues: Boolean = false
): View.OnLayoutChangeListener {
return object : View.OnLayoutChangeListener {
override fun onLayoutChange(
@@ -186,21 +258,21 @@ class ViewHierarchyAnimator {
top: Int,
right: Int,
bottom: Int,
- oldLeft: Int,
- oldTop: Int,
- oldRight: Int,
- oldBottom: Int
+ previousLeft: Int,
+ previousTop: Int,
+ previousRight: Int,
+ previousBottom: Int
) {
if (view == null) return
- val startLeft = getBound(view, Bound.LEFT) ?: oldLeft
- val startTop = getBound(view, Bound.TOP) ?: oldTop
- val startRight = getBound(view, Bound.RIGHT) ?: oldRight
- val startBottom = getBound(view, Bound.BOTTOM) ?: oldBottom
+ val startLeft = getBound(view, Bound.LEFT) ?: previousLeft
+ val startTop = getBound(view, Bound.TOP) ?: previousTop
+ val startRight = getBound(view, Bound.RIGHT) ?: previousRight
+ val startBottom = getBound(view, Bound.BOTTOM) ?: previousBottom
(view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
- if (view.visibility == View.GONE || view.visibility == View.INVISIBLE) {
+ if (!isVisible(view.visibility, left, top, right, bottom)) {
setBound(view, Bound.LEFT, left)
setBound(view, Bound.TOP, top)
setBound(view, Bound.RIGHT, right)
@@ -208,11 +280,17 @@ class ViewHierarchyAnimator {
return
}
- val startValues = mapOf(
- Bound.LEFT to startLeft,
- Bound.TOP to startTop,
- Bound.RIGHT to startRight,
- Bound.BOTTOM to startBottom
+ val startValues = processStartValues(
+ origin,
+ left,
+ top,
+ right,
+ bottom,
+ startLeft,
+ startTop,
+ startRight,
+ startBottom,
+ ignorePreviousValues
)
val endValues = mapOf(
Bound.LEFT to left,
@@ -221,11 +299,12 @@ class ViewHierarchyAnimator {
Bound.BOTTOM to bottom
)
- val boundsToAnimate = bounds.toMutableSet()
- if (left == startLeft) boundsToAnimate.remove(Bound.LEFT)
- if (top == startTop) boundsToAnimate.remove(Bound.TOP)
- if (right == startRight) boundsToAnimate.remove(Bound.RIGHT)
- if (bottom == startBottom) boundsToAnimate.remove(Bound.BOTTOM)
+ val boundsToAnimate = mutableSetOf<Bound>()
+ bounds.forEach { bound ->
+ if (endValues.getValue(bound) != startValues.getValue(bound)) {
+ boundsToAnimate.add(bound)
+ }
+ }
if (boundsToAnimate.isNotEmpty()) {
startAnimation(
@@ -242,11 +321,136 @@ class ViewHierarchyAnimator {
}
}
+ /**
+ * Returns whether the given [visibility] and bounds are consistent with a view being
+ * currently visible on screen.
+ */
+ private fun isVisible(
+ visibility: Int,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int
+ ): Boolean {
+ return visibility == View.VISIBLE && left != right && top != bottom
+ }
+
+ /**
+ * Compute 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
+ * the same as the new values if [ignorePreviousValues] is true. If [origin] is not null,
+ * the start values are resolved based on it, and [ignorePreviousValues] controls whether or
+ * not newly introduced margins are included.
+ *
+ * Base case
+ * 1) origin=TOP
+ * x---------x x---------x x---------x x---------x x---------x
+ * x---------x | | | | | |
+ * -> -> x---------x -> | | -> | |
+ * x---------x | |
+ * x---------x
+ * 2) origin=BOTTOM_LEFT
+ * x---------x
+ * x-------x | |
+ * -> -> x----x -> | | -> | |
+ * x--x | | | | | |
+ * x x--x x----x x-------x x---------x
+ * 3) origin=CENTER
+ * x---------x
+ * x-----x x-------x | |
+ * x -> x---x -> | | -> | | -> | |
+ * x-----x x-------x | |
+ * x---------x
+ *
+ * In case the start and end values differ in the direction of the origin, and
+ * [ignorePreviousValues] is false, the previous values are used and a translation is
+ * included in addition to the view expansion.
+ *
+ * origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70)
+ * x
+ * x--x
+ * x--x x----x
+ * -> -> | | -> x------x
+ * x----x | |
+ * | |
+ * x------x
+ */
+ private fun processStartValues(
+ origin: Hotspot?,
+ newLeft: Int,
+ newTop: Int,
+ newRight: Int,
+ newBottom: Int,
+ previousLeft: Int,
+ previousTop: Int,
+ previousRight: Int,
+ previousBottom: Int,
+ ignorePreviousValues: Boolean
+ ): Map<Bound, Int> {
+ val startLeft = if (ignorePreviousValues) newLeft else previousLeft
+ val startTop = if (ignorePreviousValues) newTop else previousTop
+ val startRight = if (ignorePreviousValues) newRight else previousRight
+ val startBottom = if (ignorePreviousValues) newBottom else previousBottom
+
+ var left = startLeft
+ var top = startTop
+ var right = startRight
+ var bottom = startBottom
+
+ if (origin != null) {
+ left = when (origin) {
+ Hotspot.CENTER -> (newLeft + newRight) / 2
+ Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft)
+ Hotspot.TOP, Hotspot.BOTTOM -> newLeft
+ Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max(
+ startRight,
+ newRight
+ )
+ }
+ top = when (origin) {
+ Hotspot.CENTER -> (newTop + newBottom) / 2
+ Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop)
+ Hotspot.LEFT, Hotspot.RIGHT -> newTop
+ Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max(
+ startBottom,
+ newBottom
+ )
+ }
+ right = when (origin) {
+ Hotspot.CENTER -> (newLeft + newRight) / 2
+ Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max(
+ startRight,
+ newRight
+ )
+ Hotspot.TOP, Hotspot.BOTTOM -> newRight
+ Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft)
+ }
+ bottom = when (origin) {
+ Hotspot.CENTER -> (newTop + newBottom) / 2
+ Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max(
+ startBottom,
+ newBottom
+ )
+ Hotspot.LEFT, Hotspot.RIGHT -> newBottom
+ Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop)
+ }
+ }
+
+ return mapOf(
+ Bound.LEFT to left,
+ Bound.TOP to top,
+ Bound.RIGHT to right,
+ Bound.BOTTOM to bottom
+ )
+ }
+
private fun recursivelyAddListener(view: View, listener: View.OnLayoutChangeListener) {
// Make sure that only one listener is active at a time.
- val oldListener = view.getTag(R.id.tag_layout_listener)
- if (oldListener != null && oldListener is View.OnLayoutChangeListener) {
- view.removeOnLayoutChangeListener(oldListener)
+ val previousListener = view.getTag(R.id.tag_layout_listener)
+ if (previousListener != null && previousListener is View.OnLayoutChangeListener) {
+ view.removeOnLayoutChangeListener(previousListener)
}
view.addOnLayoutChangeListener(listener)
@@ -268,9 +472,12 @@ class ViewHierarchyAnimator {
}
/**
- * Initiates the animation of a single bound by creating the animator, registering it with
- * the [view], and starting it. If [ephemeral], the layout change listener is unregistered
- * at the end of the animation, so no more animations happen.
+ * Initiates the animation of the requested [bounds] between [startValues] and [endValues]
+ * by creating the animator, registering it with the [view], and starting it using
+ * [interpolator] and [duration].
+ *
+ * If [ephemeral] is true, the layout change listener is unregistered at the end of the
+ * animation, so no more animations happen.
*/
private fun startAnimation(
view: View,
@@ -325,4 +532,52 @@ class ViewHierarchyAnimator {
animator.start()
}
}
+
+ /** An enum used to determine the origin of addition animations. */
+ enum class Hotspot {
+ CENTER, LEFT, TOP_LEFT, TOP, TOP_RIGHT, RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT
+ }
+
+ // TODO(b/221418522): make this private once it can't be passed as an arg anymore.
+ enum class Bound(val label: String, val overrideTag: Int) {
+ LEFT("left", R.id.tag_override_left) {
+ override fun setValue(view: View, value: Int) {
+ view.left = value
+ }
+
+ override fun getValue(view: View): Int {
+ return view.left
+ }
+ },
+ TOP("top", R.id.tag_override_top) {
+ override fun setValue(view: View, value: Int) {
+ view.top = value
+ }
+
+ override fun getValue(view: View): Int {
+ return view.top
+ }
+ },
+ RIGHT("right", R.id.tag_override_right) {
+ override fun setValue(view: View, value: Int) {
+ view.right = value
+ }
+
+ override fun getValue(view: View): Int {
+ return view.right
+ }
+ },
+ BOTTOM("bottom", R.id.tag_override_bottom) {
+ override fun setValue(view: View, value: Int) {
+ view.bottom = value
+ }
+
+ override fun getValue(view: View): Int {
+ return view.bottom
+ }
+ };
+
+ abstract fun setValue(view: View, value: Int)
+ abstract fun getValue(view: View): Int
+ }
}
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 8eb0918beedf..98d57a3c5da8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -9,8 +9,10 @@ import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNotNull
import junit.framework.Assert.assertNull
+import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -25,15 +27,23 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
private val TEST_INTERPOLATOR = Interpolators.LINEAR
}
+ private val childParams = LinearLayout.LayoutParams(
+ 0 /* width */,
+ LinearLayout.LayoutParams.MATCH_PARENT
+ )
private lateinit var rootView: LinearLayout
@Before
fun setUp() {
rootView = LinearLayout(mContext)
+ rootView.orientation = LinearLayout.HORIZONTAL
+ rootView.weightSum = 1f
+ childParams.weight = 0.5f
}
@After
fun tearDown() {
+ endAnimation(rootView)
ViewHierarchyAnimator.stopAnimating(rootView)
}
@@ -41,13 +51,45 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
fun respectsAnimationParameters() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animate(
+ // animate()
+ var success = ViewHierarchyAnimator.animate(
rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
)
rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)
+ assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
- val animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
+ var animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
+ assertEquals(animator.interpolator, TEST_INTERPOLATOR)
+ assertEquals(animator.duration, TEST_DURATION)
+
+ endAnimation(rootView)
+ ViewHierarchyAnimator.stopAnimating(rootView)
+
+ // animateNextUpdate()
+ success = ViewHierarchyAnimator.animateNextUpdate(
+ rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
+ )
+ rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
+ assertEquals(animator.interpolator, TEST_INTERPOLATOR)
+ assertEquals(animator.duration, TEST_DURATION)
+
+ endAnimation(rootView)
+
+ // animateAddition()
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
+ )
+ rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
assertEquals(animator.interpolator, TEST_INTERPOLATOR)
assertEquals(animator.duration, TEST_DURATION)
}
@@ -56,10 +98,11 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
fun animatesFromStartToEnd() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animate(rootView)
+ val success = ViewHierarchyAnimator.animate(rootView)
// Change all bounds.
rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)
+ assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
// The initial values should be those of the previous layout.
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
@@ -73,10 +116,11 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
fun animatesSuccessiveLayoutChanges() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animate(rootView)
+ val success = ViewHierarchyAnimator.animate(rootView)
// Change all bounds.
rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)
+ assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
endAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_animator))
@@ -103,10 +147,12 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
fun animatesFromPreviousAnimationProgress() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animateNextUpdate(rootView, interpolator = TEST_INTERPOLATOR)
+ val success =
+ ViewHierarchyAnimator.animateNextUpdate(rootView, interpolator = TEST_INTERPOLATOR)
// Change all bounds.
rootView.layout(0 /* l */, 20 /* t */, 70 /* r */, 80 /* b */)
+ assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
advanceAnimation(rootView, fraction = 0.5f)
checkBounds(rootView, l = 5, t = 15, r = 60, b = 65)
@@ -124,57 +170,448 @@ class 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 */)
- firstChild.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)
- secondChild.layout(100 /* l */, 0 /* t */, 150 /* r */, 100 /* b */)
- ViewHierarchyAnimator.animate(rootView)
+ val success = ViewHierarchyAnimator.animate(rootView)
// Change all bounds.
+ rootView.measure(
+ View.MeasureSpec.makeMeasureSpec(190, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ )
rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)
- firstChild.layout(10 /* l */, 20 /* t */, 150 /* r */, 120 /* b */)
- secondChild.layout(150 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)
+ assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
+ assertNotNull(firstChild.getTag(R.id.tag_animator))
+ assertNotNull(secondChild.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 = 100, b = 100)
- checkBounds(secondChild, l = 100, 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)
endAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_animator))
+ assertNull(firstChild.getTag(R.id.tag_animator))
+ assertNull(secondChild.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 = 10, t = 20, r = 150, b = 120)
- checkBounds(secondChild, l = 150, 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)
+ }
+
+ @Test
+ fun animatesAppearingViewsFromStartToEnd() {
+ // Starting GONE.
+ rootView.visibility = View.GONE
+ rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
+ var success = ViewHierarchyAnimator.animateAddition(rootView)
+ rootView.visibility = View.VISIBLE
+ rootView.layout(0 /* l */, 100 /* t */, 100 /* r */, 200 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 150, r = 50, b = 150)
+ endAnimation(rootView)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 100, r = 100, b = 200)
+
+ // Starting INVISIBLE.
+ rootView.visibility = View.INVISIBLE
+ rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(rootView)
+ rootView.visibility = View.VISIBLE
+ rootView.layout(0 /* l */, 100 /* t */, 100 /* r */, 200 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 150, r = 50, b = 150)
+ endAnimation(rootView)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 100, r = 100, b = 200)
+
+ // Starting with nothing.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(rootView)
+ rootView.layout(0 /* l */, 20 /* t */, 50 /* r */, 80 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 25, t = 50, r = 25, b = 50)
+ endAnimation(rootView)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 20, r = 50, b = 80)
+
+ // Starting with 0 width.
+ rootView.layout(0 /* l */, 50 /* t */, 0 /* r */, 100 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(rootView)
+ rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 25, t = 75, r = 25, b = 75)
+ endAnimation(rootView)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 50, r = 50, b = 100)
+
+ // Starting with 0 height.
+ rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 50 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(rootView)
+ rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 25, t = 75, r = 25, b = 75)
+ endAnimation(rootView)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 50, r = 50, b = 100)
+ }
+
+ @Test
+ fun animatesAppearingViewsRespectingOrigin() {
+ // CENTER.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ var success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.CENTER
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 75, t = 75, r = 75, b = 75)
+ endAnimation(rootView)
+
+ // LEFT.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.LEFT
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 50, r = 50, b = 100)
+ endAnimation(rootView)
+
+ // TOP_LEFT.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 50, r = 50, b = 50)
+ endAnimation(rootView)
+
+ // TOP.
+ rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.TOP
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 50, r = 100, b = 50)
+ endAnimation(rootView)
+
+ // TOP_RIGHT.
+ rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 100, t = 50, r = 100, b = 50)
+ endAnimation(rootView)
+
+ // RIGHT.
+ rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.RIGHT
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 100, t = 50, r = 100, b = 100)
+ endAnimation(rootView)
+
+ // BOTTOM_RIGHT.
+ rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 100, t = 100, r = 100, b = 100)
+ endAnimation(rootView)
+
+ // BOTTOM.
+ rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.BOTTOM
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 100, r = 100, b = 100)
+ endAnimation(rootView)
+
+ // BOTTOM_LEFT.
+ rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 100, r = 50, b = 100)
+ endAnimation(rootView)
+ }
+
+ @Test
+ fun animatesAppearingViewsRespectingMargins() {
+ // CENTER.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ var success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.CENTER,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 75, t = 75, r = 75, b = 75)
+ endAnimation(rootView)
+
+ // LEFT.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView, origin = ViewHierarchyAnimator.Hotspot.LEFT,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 50, r = 0, b = 100)
+ endAnimation(rootView)
+
+ // TOP_LEFT.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 0, r = 0, b = 0)
+ endAnimation(rootView)
+
+ // TOP.
+ rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView, origin = ViewHierarchyAnimator.Hotspot.TOP,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 0, r = 100, b = 0)
+ endAnimation(rootView)
+
+ // TOP_RIGHT.
+ rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 150, t = 0, r = 150, b = 0)
+ endAnimation(rootView)
+
+ // RIGHT.
+ rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.RIGHT,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 150, t = 50, r = 150, b = 100)
+ endAnimation(rootView)
+
+ // BOTTOM_RIGHT.
+ rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 150, t = 150, r = 150, b = 150)
+ endAnimation(rootView)
+
+ // BOTTOM.
+ rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.BOTTOM,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 50, t = 150, r = 100, b = 150)
+ endAnimation(rootView)
+
+ // BOTTOM_LEFT.
+ rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
+ success = ViewHierarchyAnimator.animateAddition(
+ rootView,
+ origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
+ includeMargins = true
+ )
+ rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 150, r = 0, b = 150)
+ endAnimation(rootView)
}
@Test
fun doesNotAnimateInvisibleViews() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animate(rootView)
- // GONE.
+ // GONE
rootView.visibility = View.GONE
+ var success = ViewHierarchyAnimator.animate(rootView)
rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)
+ assertFalse(success)
assertNull(rootView.getTag(R.id.tag_animator))
checkBounds(rootView, l = 0, t = 15, r = 55, b = 80)
// INVISIBLE.
rootView.visibility = View.INVISIBLE
- rootView.layout(0 /* l */, 20 /* t */, 0 /* r */, 20 /* b */)
+ success = ViewHierarchyAnimator.animate(rootView)
+ rootView.layout(0 /* l */, 20 /* t */, 10 /* r */, 50 /* b */)
+
+ assertFalse(success)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 20, r = 10, b = 50)
+ }
+
+ @Test
+ fun doesNotAnimateAppearingViews() {
+ // Starting with nothing.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+ var success = ViewHierarchyAnimator.animate(rootView)
+ rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)
+
+ assertFalse(success)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 15, r = 55, b = 80)
+
+ // Starting with 0 width.
+ rootView.layout(0 /* l */, 50 /* t */, 0 /* r */, 100 /* b */)
+ success = ViewHierarchyAnimator.animate(rootView)
+ rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)
+
+ assertFalse(success)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 15, r = 55, b = 80)
+
+ // Starting with 0 height.
+ rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 50 /* b */)
+ success = ViewHierarchyAnimator.animate(rootView)
+ rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)
+
+ assertFalse(success)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 15, r = 55, b = 80)
+ }
+
+ @Test
+ fun doesNotAnimateDisappearingViews() {
+ rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)
+
+ val success = ViewHierarchyAnimator.animate(rootView)
+ // Ending with nothing.
+ rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
+
+ assertTrue(success)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 0, r = 0, b = 0)
+
+ // Ending with 0 width.
+ rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
+ endAnimation(rootView)
+ rootView.layout(0 /* l */, 15 /* t */, 0 /* r */, 80 /* b */)
+
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 15, r = 0, b = 80)
+
+ // Ending with 0 height.
+ rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
+ endAnimation(rootView)
+ rootView.layout(0 /* l */, 50 /* t */, 55 /* r */, 50 /* b */)
+
+ assertNull(rootView.getTag(R.id.tag_animator))
+ checkBounds(rootView, l = 0, t = 50, r = 55, b = 50)
}
@Test
fun doesNotAnimateUnchangingBounds() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animate(rootView)
+ val success = ViewHierarchyAnimator.animate(rootView)
// No bounds are changed.
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
+ assertTrue(success)
assertNull(rootView.getTag(R.id.tag_animator))
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
@@ -191,7 +628,7 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
fun doesNotAnimateExcludedBounds() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animate(
+ val success = ViewHierarchyAnimator.animate(
rootView,
bounds = setOf(ViewHierarchyAnimator.Bound.LEFT, ViewHierarchyAnimator.Bound.TOP),
interpolator = TEST_INTERPOLATOR
@@ -199,6 +636,7 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
// Change all bounds.
rootView.layout(0 /* l */, 20 /* t */, 70 /* r */, 80 /* b */)
+ assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
advanceAnimation(rootView, 0.5f)
checkBounds(rootView, l = 5, t = 15, r = 70, b = 80)
@@ -211,10 +649,11 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
fun stopsAnimatingAfterSingleLayout() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animateNextUpdate(rootView)
+ val success = ViewHierarchyAnimator.animateNextUpdate(rootView)
// Change all bounds.
rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)
+ assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
endAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_animator))
@@ -231,10 +670,11 @@ class ViewHierarchyAnimatorTest : SysuiTestCase() {
fun stopsAnimatingWhenInstructed() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
- ViewHierarchyAnimator.animate(rootView)
+ val success = ViewHierarchyAnimator.animate(rootView)
// Change all bounds.
rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)
+ assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
endAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_animator))