diff options
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)) |