summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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))