Merge "Add animateAddition() to the ViewBoundAnimator API." into tm-dev

This commit is contained in:
Luca Zuccarini
2022-03-31 15:30:02 +00:00
committed by Android (Google) Code Review
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))