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
This commit is contained in:
Luca Zuccarini
2022-03-18 15:44:39 +00:00
parent ce40772220
commit 3ccbe8c15a
2 changed files with 792 additions and 97 deletions

View File

@@ -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
}
}

View File

@@ -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))