diff options
| author | 2022-03-18 15:44:39 +0000 | |
|---|---|---|
| committer | 2022-03-30 14:43:52 +0000 | |
| commit | 3ccbe8c15ac7b6800ea90dffcd5386fcce58116e (patch) | |
| tree | 3e8f43cb1f7ae75ac41159f6ff83bf732bae6707 | |
| parent | ce407722207b15fc4e3d1ba0001681c9e2c08599 (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
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)) |