Merge "Add animateRemoval() to the ViewBoundAnimator API." into tm-dev am: f2bbd4e746 am: b1e3f5909b am: 522e004d3d
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17449405 Change-Id: I64e8f3f9f511f7da69bdd0eae452e46f45313cba Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import android.animation.Animator
|
|||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.animation.PropertyValuesHolder
|
import android.animation.PropertyValuesHolder
|
||||||
|
import android.animation.ValueAnimator
|
||||||
import android.util.IntProperty
|
import android.util.IntProperty
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -37,6 +38,7 @@ class ViewHierarchyAnimator {
|
|||||||
private const val DEFAULT_DURATION = 500L
|
private const val DEFAULT_DURATION = 500L
|
||||||
private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD
|
private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD
|
||||||
private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
|
private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
|
||||||
|
private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE
|
||||||
|
|
||||||
/** The properties used to animate the view bounds. */
|
/** The properties used to animate the view bounds. */
|
||||||
private val PROPERTIES = mapOf(
|
private val PROPERTIES = mapOf(
|
||||||
@@ -113,7 +115,7 @@ class ViewHierarchyAnimator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val listener = createUpdateListener(interpolator, duration, ephemeral)
|
val listener = createUpdateListener(interpolator, duration, ephemeral)
|
||||||
recursivelyAddListener(rootView, listener)
|
addListener(rootView, listener, recursive = true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +185,7 @@ class ViewHierarchyAnimator {
|
|||||||
val listener = createAdditionListener(
|
val listener = createAdditionListener(
|
||||||
origin, interpolator, duration, ignorePreviousValues = !includeMargins
|
origin, interpolator, duration, ignorePreviousValues = !includeMargins
|
||||||
)
|
)
|
||||||
recursivelyAddListener(rootView, listener)
|
addListener(rootView, listener, recursive = true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +299,183 @@ class ViewHierarchyAnimator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the removal of [rootView] and its children from the hierarchy. It uses the given
|
||||||
|
* [interpolator] and [duration].
|
||||||
|
*
|
||||||
|
* The end state of the animation is controlled by [destination]. This value can be any of
|
||||||
|
* the four corners, any of the four edges, or the center of the view.
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun animateRemoval(
|
||||||
|
rootView: View,
|
||||||
|
destination: Hotspot = Hotspot.CENTER,
|
||||||
|
interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
|
||||||
|
duration: Long = DEFAULT_DURATION
|
||||||
|
): Boolean {
|
||||||
|
if (!isVisible(
|
||||||
|
rootView.visibility,
|
||||||
|
rootView.left,
|
||||||
|
rootView.top,
|
||||||
|
rootView.right,
|
||||||
|
rootView.bottom
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val parent = rootView.parent as ViewGroup
|
||||||
|
|
||||||
|
// Ensure that rootView's siblings animate nicely around the removal.
|
||||||
|
val listener = createUpdateListener(
|
||||||
|
interpolator,
|
||||||
|
duration,
|
||||||
|
ephemeral = true
|
||||||
|
)
|
||||||
|
for (i in 0 until parent.childCount) {
|
||||||
|
val child = parent.getChildAt(i)
|
||||||
|
if (child == rootView) continue
|
||||||
|
addListener(child, listener, recursive = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the view so that a layout update is triggered for the siblings and they
|
||||||
|
// animate to their next position while the view's removal is also animating.
|
||||||
|
parent.removeView(rootView)
|
||||||
|
// By adding the view to the overlay, we can animate it while it isn't part of the view
|
||||||
|
// hierarchy. It is correctly positioned because we have its previous bounds, and we set
|
||||||
|
// them manually during the animation.
|
||||||
|
parent.overlay.add(rootView)
|
||||||
|
|
||||||
|
val startValues = mapOf(
|
||||||
|
Bound.LEFT to rootView.left,
|
||||||
|
Bound.TOP to rootView.top,
|
||||||
|
Bound.RIGHT to rootView.right,
|
||||||
|
Bound.BOTTOM to rootView.bottom
|
||||||
|
)
|
||||||
|
val endValues = processEndValuesForRemoval(
|
||||||
|
destination,
|
||||||
|
rootView.left,
|
||||||
|
rootView.top,
|
||||||
|
rootView.right,
|
||||||
|
rootView.bottom
|
||||||
|
)
|
||||||
|
|
||||||
|
val boundsToAnimate = mutableSetOf<Bound>()
|
||||||
|
if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT)
|
||||||
|
if (rootView.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP)
|
||||||
|
if (rootView.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT)
|
||||||
|
if (rootView.bottom != endValues.getValue(Bound.BOTTOM)) {
|
||||||
|
boundsToAnimate.add(Bound.BOTTOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimation(
|
||||||
|
rootView,
|
||||||
|
boundsToAnimate,
|
||||||
|
startValues,
|
||||||
|
endValues,
|
||||||
|
interpolator,
|
||||||
|
duration,
|
||||||
|
ephemeral = true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rootView is ViewGroup) {
|
||||||
|
// Shift the children so they maintain a consistent position within the shrinking
|
||||||
|
// view.
|
||||||
|
shiftChildrenForRemoval(rootView, destination, endValues, interpolator, duration)
|
||||||
|
|
||||||
|
// Fade out the children during the first half of the removal, so they don't clutter
|
||||||
|
// too much once the view becomes very small. Then we fade out the view itself, in
|
||||||
|
// case it has its own content and/or background.
|
||||||
|
val startAlphas = FloatArray(rootView.childCount)
|
||||||
|
for (i in 0 until rootView.childCount) {
|
||||||
|
startAlphas[i] = rootView.getChildAt(i).alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
val animator = ValueAnimator.ofFloat(1f, 0f)
|
||||||
|
animator.interpolator = Interpolators.ALPHA_OUT
|
||||||
|
animator.duration = duration / 2
|
||||||
|
animator.addUpdateListener { animation ->
|
||||||
|
for (i in 0 until rootView.childCount) {
|
||||||
|
rootView.getChildAt(i).alpha =
|
||||||
|
(animation.animatedValue as Float) * startAlphas[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animator.addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
rootView.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setInterpolator(Interpolators.ALPHA_OUT)
|
||||||
|
.setDuration(duration / 2)
|
||||||
|
.withEndAction { parent.overlay.remove(rootView) }
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
animator.start()
|
||||||
|
} else {
|
||||||
|
// Fade out the view during the second half of the removal.
|
||||||
|
rootView.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setInterpolator(Interpolators.ALPHA_OUT)
|
||||||
|
.setDuration(duration / 2)
|
||||||
|
.setStartDelay(duration / 2)
|
||||||
|
.withEndAction { parent.overlay.remove(rootView) }
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the children of [rootView] so that its layout remains internally consistent as
|
||||||
|
* it shrinks towards [destination] and changes its bounds to [endValues].
|
||||||
|
*
|
||||||
|
* Uses [interpolator] and [duration], which should match those of the removal animation.
|
||||||
|
*/
|
||||||
|
private fun shiftChildrenForRemoval(
|
||||||
|
rootView: ViewGroup,
|
||||||
|
destination: Hotspot,
|
||||||
|
endValues: Map<Bound, Int>,
|
||||||
|
interpolator: Interpolator,
|
||||||
|
duration: Long
|
||||||
|
) {
|
||||||
|
for (i in 0 until rootView.childCount) {
|
||||||
|
val child = rootView.getChildAt(i)
|
||||||
|
val childStartValues = mapOf(
|
||||||
|
Bound.LEFT to child.left,
|
||||||
|
Bound.TOP to child.top,
|
||||||
|
Bound.RIGHT to child.right,
|
||||||
|
Bound.BOTTOM to child.bottom
|
||||||
|
)
|
||||||
|
val childEndValues = processChildEndValuesForRemoval(
|
||||||
|
destination,
|
||||||
|
child.left,
|
||||||
|
child.top,
|
||||||
|
child.right,
|
||||||
|
child.bottom,
|
||||||
|
endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT),
|
||||||
|
endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP)
|
||||||
|
)
|
||||||
|
|
||||||
|
val boundsToAnimate = mutableSetOf<Bound>()
|
||||||
|
if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT)
|
||||||
|
if (child.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP)
|
||||||
|
if (child.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT)
|
||||||
|
if (child.bottom != endValues.getValue(Bound.BOTTOM)) {
|
||||||
|
boundsToAnimate.add(Bound.BOTTOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimation(
|
||||||
|
child,
|
||||||
|
boundsToAnimate,
|
||||||
|
childStartValues,
|
||||||
|
childEndValues,
|
||||||
|
interpolator,
|
||||||
|
duration,
|
||||||
|
ephemeral = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the given [visibility] and bounds are consistent with a view being
|
* Returns whether the given [visibility] and bounds are consistent with a view being
|
||||||
* currently visible on screen.
|
* currently visible on screen.
|
||||||
@@ -312,7 +491,7 @@ class ViewHierarchyAnimator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the actual starting values based on the requested [origin] and on
|
* Computes the actual starting values based on the requested [origin] and on
|
||||||
* [ignorePreviousValues].
|
* [ignorePreviousValues].
|
||||||
*
|
*
|
||||||
* If [origin] is null, the resolved start values will be the same as those passed in, or
|
* If [origin] is null, the resolved start values will be the same as those passed in, or
|
||||||
@@ -422,7 +601,140 @@ class ViewHierarchyAnimator {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recursivelyAddListener(view: View, listener: View.OnLayoutChangeListener) {
|
/**
|
||||||
|
* Computes a removal animation's end values based on the requested [destination] and the
|
||||||
|
* view's starting bounds.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* 1) destination=TOP
|
||||||
|
* x---------x x---------x x---------x x---------x x---------x
|
||||||
|
* | | | | | | x---------x
|
||||||
|
* | | -> | | -> x---------x -> ->
|
||||||
|
* | | x---------x
|
||||||
|
* x---------x
|
||||||
|
* 2) destination=BOTTOM_LEFT
|
||||||
|
* x---------x
|
||||||
|
* | | x-------x
|
||||||
|
* | | -> | | -> x----x -> ->
|
||||||
|
* | | | | | | x--x
|
||||||
|
* x---------x x-------x x----x x--x x
|
||||||
|
* 3) destination=CENTER
|
||||||
|
* x---------x
|
||||||
|
* | | x-------x x-----x
|
||||||
|
* | | -> | | -> | | -> x---x -> x
|
||||||
|
* | | x-------x x-----x
|
||||||
|
* x---------x
|
||||||
|
*/
|
||||||
|
private fun processEndValuesForRemoval(
|
||||||
|
destination: Hotspot,
|
||||||
|
left: Int,
|
||||||
|
top: Int,
|
||||||
|
right: Int,
|
||||||
|
bottom: Int
|
||||||
|
): Map<Bound, Int> {
|
||||||
|
val endLeft = when (destination) {
|
||||||
|
Hotspot.CENTER -> (left + right) / 2
|
||||||
|
Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP ->
|
||||||
|
left
|
||||||
|
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> right
|
||||||
|
}
|
||||||
|
val endTop = when (destination) {
|
||||||
|
Hotspot.CENTER -> (top + bottom) / 2
|
||||||
|
Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT ->
|
||||||
|
top
|
||||||
|
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> bottom
|
||||||
|
}
|
||||||
|
val endRight = when (destination) {
|
||||||
|
Hotspot.CENTER -> (left + right) / 2
|
||||||
|
Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT,
|
||||||
|
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM ->
|
||||||
|
right
|
||||||
|
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> left
|
||||||
|
}
|
||||||
|
val endBottom = when (destination) {
|
||||||
|
Hotspot.CENTER -> (top + bottom) / 2
|
||||||
|
Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM,
|
||||||
|
Hotspot.BOTTOM_LEFT, Hotspot.LEFT ->
|
||||||
|
bottom
|
||||||
|
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> top
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
Bound.LEFT to endLeft,
|
||||||
|
Bound.TOP to endTop,
|
||||||
|
Bound.RIGHT to endRight,
|
||||||
|
Bound.BOTTOM to endBottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the end values for the child of a view being removed, based on the child's
|
||||||
|
* starting bounds, the removal's [destination], and the [parentWidth] and [parentHeight].
|
||||||
|
*
|
||||||
|
* The end values always represent the child's position after it has been translated so that
|
||||||
|
* its center is at the [destination].
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* 1) destination=TOP
|
||||||
|
* The child maintains its left and right positions, but is shifted up so that its
|
||||||
|
* center is on the parent's end top edge.
|
||||||
|
* 2) destination=BOTTOM_LEFT
|
||||||
|
* The child shifts so that its center is on the parent's end bottom left corner.
|
||||||
|
* 3) destination=CENTER
|
||||||
|
* The child shifts so that its own center is on the parent's end center.
|
||||||
|
*/
|
||||||
|
private fun processChildEndValuesForRemoval(
|
||||||
|
destination: Hotspot,
|
||||||
|
left: Int,
|
||||||
|
top: Int,
|
||||||
|
right: Int,
|
||||||
|
bottom: Int,
|
||||||
|
parentWidth: Int,
|
||||||
|
parentHeight: Int
|
||||||
|
): Map<Bound, Int> {
|
||||||
|
val halfWidth = (right - left) / 2
|
||||||
|
val halfHeight = (bottom - top) / 2
|
||||||
|
|
||||||
|
val endLeft = when (destination) {
|
||||||
|
Hotspot.CENTER -> (parentWidth / 2) - halfWidth
|
||||||
|
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> -halfWidth
|
||||||
|
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth
|
||||||
|
Hotspot.TOP, Hotspot.BOTTOM -> left
|
||||||
|
}
|
||||||
|
val endTop = when (destination) {
|
||||||
|
Hotspot.CENTER -> (parentHeight / 2) - halfHeight
|
||||||
|
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> -halfHeight
|
||||||
|
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT ->
|
||||||
|
parentHeight - halfHeight
|
||||||
|
Hotspot.LEFT, Hotspot.RIGHT -> top
|
||||||
|
}
|
||||||
|
val endRight = when (destination) {
|
||||||
|
Hotspot.CENTER -> (parentWidth / 2) + halfWidth
|
||||||
|
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth
|
||||||
|
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> halfWidth
|
||||||
|
Hotspot.TOP, Hotspot.BOTTOM -> right
|
||||||
|
}
|
||||||
|
val endBottom = when (destination) {
|
||||||
|
Hotspot.CENTER -> (parentHeight / 2) + halfHeight
|
||||||
|
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT ->
|
||||||
|
parentHeight + halfHeight
|
||||||
|
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> halfHeight
|
||||||
|
Hotspot.LEFT, Hotspot.RIGHT -> bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
Bound.LEFT to endLeft,
|
||||||
|
Bound.TOP to endTop,
|
||||||
|
Bound.RIGHT to endRight,
|
||||||
|
Bound.BOTTOM to endBottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addListener(
|
||||||
|
view: View,
|
||||||
|
listener: View.OnLayoutChangeListener,
|
||||||
|
recursive: Boolean = false
|
||||||
|
) {
|
||||||
// Make sure that only one listener is active at a time.
|
// Make sure that only one listener is active at a time.
|
||||||
val previousListener = view.getTag(R.id.tag_layout_listener)
|
val previousListener = view.getTag(R.id.tag_layout_listener)
|
||||||
if (previousListener != null && previousListener is View.OnLayoutChangeListener) {
|
if (previousListener != null && previousListener is View.OnLayoutChangeListener) {
|
||||||
@@ -431,9 +743,9 @@ class ViewHierarchyAnimator {
|
|||||||
|
|
||||||
view.addOnLayoutChangeListener(listener)
|
view.addOnLayoutChangeListener(listener)
|
||||||
view.setTag(R.id.tag_layout_listener, listener)
|
view.setTag(R.id.tag_layout_listener, listener)
|
||||||
if (view is ViewGroup) {
|
if (view is ViewGroup && recursive) {
|
||||||
for (i in 0 until view.childCount) {
|
for (i in 0 until view.childCount) {
|
||||||
recursivelyAddListener(view.getChildAt(i), listener)
|
addListener(view.getChildAt(i), listener, recursive = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -490,6 +802,8 @@ class ViewHierarchyAnimator {
|
|||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
|
(view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
|
||||||
|
|
||||||
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
|
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
|
||||||
animator.interpolator = interpolator
|
animator.interpolator = interpolator
|
||||||
animator.duration = duration
|
animator.duration = duration
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import android.testing.TestableLooper
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.RelativeLayout
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.util.children
|
||||||
import junit.framework.Assert.assertEquals
|
import junit.framework.Assert.assertEquals
|
||||||
import junit.framework.Assert.assertFalse
|
import junit.framework.Assert.assertFalse
|
||||||
import junit.framework.Assert.assertNotNull
|
import junit.framework.Assert.assertNotNull
|
||||||
@@ -28,18 +30,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
|
|||||||
private val TEST_INTERPOLATOR = Interpolators.LINEAR
|
private val TEST_INTERPOLATOR = Interpolators.LINEAR
|
||||||
}
|
}
|
||||||
|
|
||||||
private val childParams = LinearLayout.LayoutParams(
|
private lateinit var rootView: ViewGroup
|
||||||
0 /* width */,
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
private lateinit var rootView: LinearLayout
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
rootView = LinearLayout(mContext)
|
rootView = LinearLayout(mContext)
|
||||||
rootView.orientation = LinearLayout.HORIZONTAL
|
|
||||||
rootView.weightSum = 1f
|
|
||||||
childParams.weight = 0.5f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -93,6 +88,19 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
|
|||||||
animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
|
animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
|
||||||
assertEquals(animator.interpolator, TEST_INTERPOLATOR)
|
assertEquals(animator.interpolator, TEST_INTERPOLATOR)
|
||||||
assertEquals(animator.duration, TEST_DURATION)
|
assertEquals(animator.duration, TEST_DURATION)
|
||||||
|
|
||||||
|
// animateRemoval()
|
||||||
|
setUpRootWithChildren()
|
||||||
|
val child = rootView.getChildAt(0)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
child, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(child.getTag(R.id.tag_animator))
|
||||||
|
animator = child.getTag(R.id.tag_animator) as ObjectAnimator
|
||||||
|
assertEquals(animator.interpolator, TEST_INTERPOLATOR)
|
||||||
|
assertEquals(animator.duration, TEST_DURATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -170,17 +178,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun animatesRootAndChildren() {
|
fun animatesRootAndChildren() {
|
||||||
val firstChild = View(mContext)
|
setUpRootWithChildren()
|
||||||
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 */)
|
|
||||||
|
|
||||||
val success = ViewHierarchyAnimator.animate(rootView)
|
val success = ViewHierarchyAnimator.animate(rootView)
|
||||||
// Change all bounds.
|
// Change all bounds.
|
||||||
@@ -192,20 +190,20 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
|
|||||||
|
|
||||||
assertTrue(success)
|
assertTrue(success)
|
||||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||||
assertNotNull(firstChild.getTag(R.id.tag_animator))
|
assertNotNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
|
||||||
assertNotNull(secondChild.getTag(R.id.tag_animator))
|
assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
|
||||||
// The initial values should be those of the previous layout.
|
// The initial values should be those of the previous layout.
|
||||||
checkBounds(rootView, l = 0, t = 0, r = 150, b = 100)
|
checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
|
||||||
checkBounds(firstChild, l = 0, t = 0, r = 75, b = 100)
|
checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 100, b = 100)
|
||||||
checkBounds(secondChild, l = 75, t = 0, r = 150, b = 100)
|
checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100)
|
||||||
endAnimation(rootView)
|
endAnimation(rootView)
|
||||||
assertNull(rootView.getTag(R.id.tag_animator))
|
assertNull(rootView.getTag(R.id.tag_animator))
|
||||||
assertNull(firstChild.getTag(R.id.tag_animator))
|
assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
|
||||||
assertNull(secondChild.getTag(R.id.tag_animator))
|
assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
|
||||||
// The end values should be those of the latest layout.
|
// The end values should be those of the latest layout.
|
||||||
checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
|
checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
|
||||||
checkBounds(firstChild, l = 0, t = 0, r = 95, b = 100)
|
checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 95, b = 100)
|
||||||
checkBounds(secondChild, l = 95, t = 0, r = 190, b = 100)
|
checkBounds(rootView.getChildAt(1), l = 95, t = 0, r = 190, b = 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -522,6 +520,251 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
|
|||||||
endAnimation(rootView)
|
endAnimation(rootView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun animatesViewRemovalFromStartToEnd() {
|
||||||
|
setUpRootWithChildren()
|
||||||
|
|
||||||
|
val child = rootView.getChildAt(0)
|
||||||
|
val success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
child,
|
||||||
|
destination = ViewHierarchyAnimator.Hotspot.LEFT,
|
||||||
|
interpolator = Interpolators.LINEAR
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(child.getTag(R.id.tag_animator))
|
||||||
|
checkBounds(child, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
advanceAnimation(child, 0.5f)
|
||||||
|
checkBounds(child, l = 0, t = 0, r = 50, b = 100)
|
||||||
|
advanceAnimation(child, 1.0f)
|
||||||
|
checkBounds(child, l = 0, t = 0, r = 0, b = 100)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(child)
|
||||||
|
assertEquals(1, rootView.childCount)
|
||||||
|
assertFalse(child in rootView.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun animatesViewRemovalRespectingDestination() {
|
||||||
|
// CENTER
|
||||||
|
setUpRootWithChildren()
|
||||||
|
var removedChild = rootView.getChildAt(0)
|
||||||
|
var remainingChild = rootView.getChildAt(1)
|
||||||
|
var success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.CENTER
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 50, t = 50, r = 50, b = 50)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
|
||||||
|
// LEFT
|
||||||
|
setUpRootWithChildren()
|
||||||
|
removedChild = rootView.getChildAt(0)
|
||||||
|
remainingChild = rootView.getChildAt(1)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.LEFT
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 0, t = 0, r = 0, b = 100)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
|
||||||
|
// TOP_LEFT
|
||||||
|
setUpRootWithChildren()
|
||||||
|
removedChild = rootView.getChildAt(0)
|
||||||
|
remainingChild = rootView.getChildAt(1)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 0, t = 0, r = 0, b = 0)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
|
||||||
|
// TOP
|
||||||
|
setUpRootWithChildren()
|
||||||
|
removedChild = rootView.getChildAt(0)
|
||||||
|
remainingChild = rootView.getChildAt(1)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 0, t = 0, r = 100, b = 0)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
|
||||||
|
// TOP_RIGHT
|
||||||
|
setUpRootWithChildren()
|
||||||
|
removedChild = rootView.getChildAt(0)
|
||||||
|
remainingChild = rootView.getChildAt(1)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 100, t = 0, r = 100, b = 0)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
|
||||||
|
// RIGHT
|
||||||
|
setUpRootWithChildren()
|
||||||
|
removedChild = rootView.getChildAt(0)
|
||||||
|
remainingChild = rootView.getChildAt(1)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.RIGHT
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 100, t = 0, r = 100, b = 100)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
|
||||||
|
// BOTTOM_RIGHT
|
||||||
|
setUpRootWithChildren()
|
||||||
|
removedChild = rootView.getChildAt(0)
|
||||||
|
remainingChild = rootView.getChildAt(1)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 100, t = 100, r = 100, b = 100)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
|
||||||
|
// BOTTOM
|
||||||
|
setUpRootWithChildren()
|
||||||
|
removedChild = rootView.getChildAt(0)
|
||||||
|
remainingChild = rootView.getChildAt(1)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 0, t = 100, r = 100, b = 100)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
|
||||||
|
// BOTTOM_LEFT
|
||||||
|
setUpRootWithChildren()
|
||||||
|
removedChild = rootView.getChildAt(0)
|
||||||
|
remainingChild = rootView.getChildAt(1)
|
||||||
|
success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(removedChild.getTag(R.id.tag_animator))
|
||||||
|
advanceAnimation(removedChild, 1.0f)
|
||||||
|
checkBounds(removedChild, l = 0, t = 100, r = 0, b = 100)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun animatesChildrenDuringViewRemoval() {
|
||||||
|
setUpRootWithChildren()
|
||||||
|
|
||||||
|
val child = rootView.getChildAt(0) as ViewGroup
|
||||||
|
val firstGrandChild = child.getChildAt(0)
|
||||||
|
val secondGrandChild = child.getChildAt(1)
|
||||||
|
val success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
child, interpolator = Interpolators.LINEAR
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(child.getTag(R.id.tag_animator))
|
||||||
|
assertNotNull(firstGrandChild.getTag(R.id.tag_animator))
|
||||||
|
assertNotNull(secondGrandChild.getTag(R.id.tag_animator))
|
||||||
|
checkBounds(child, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
checkBounds(firstGrandChild, l = 0, t = 0, r = 40, b = 40)
|
||||||
|
checkBounds(secondGrandChild, l = 60, t = 60, r = 100, b = 100)
|
||||||
|
|
||||||
|
advanceAnimation(child, 0.5f)
|
||||||
|
checkBounds(child, l = 25, t = 25, r = 75, b = 75)
|
||||||
|
checkBounds(firstGrandChild, l = -10, t = -10, r = 30, b = 30)
|
||||||
|
checkBounds(secondGrandChild, l = 20, t = 20, r = 60, b = 60)
|
||||||
|
|
||||||
|
advanceAnimation(child, 1.0f)
|
||||||
|
checkBounds(child, l = 50, t = 50, r = 50, b = 50)
|
||||||
|
checkBounds(firstGrandChild, l = -20, t = -20, r = 20, b = 20)
|
||||||
|
checkBounds(secondGrandChild, l = -20, t = -20, r = 20, b = 20)
|
||||||
|
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun animatesSiblingsDuringViewRemoval() {
|
||||||
|
setUpRootWithChildren()
|
||||||
|
|
||||||
|
val removedChild = rootView.getChildAt(0)
|
||||||
|
val remainingChild = rootView.getChildAt(1)
|
||||||
|
val success = ViewHierarchyAnimator.animateRemoval(
|
||||||
|
removedChild, interpolator = Interpolators.LINEAR
|
||||||
|
)
|
||||||
|
// Ensure that the layout happens before the checks.
|
||||||
|
forceLayout()
|
||||||
|
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(remainingChild.getTag(R.id.tag_animator))
|
||||||
|
checkBounds(remainingChild, l = 100, t = 0, r = 200, b = 100)
|
||||||
|
advanceAnimation(rootView, 0.5f)
|
||||||
|
checkBounds(remainingChild, l = 50, t = 0, r = 150, b = 100)
|
||||||
|
advanceAnimation(rootView, 1.0f)
|
||||||
|
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
|
||||||
|
endAnimation(rootView)
|
||||||
|
endAnimation(removedChild)
|
||||||
|
assertNull(remainingChild.getTag(R.id.tag_animator))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun cleansUpListenersCorrectly() {
|
fun cleansUpListenersCorrectly() {
|
||||||
val firstChild = View(mContext)
|
val firstChild = View(mContext)
|
||||||
@@ -700,6 +943,49 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
|
|||||||
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
|
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setUpRootWithChildren() {
|
||||||
|
rootView = LinearLayout(mContext)
|
||||||
|
(rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL
|
||||||
|
(rootView as LinearLayout).weightSum = 1f
|
||||||
|
|
||||||
|
val firstChild = RelativeLayout(mContext)
|
||||||
|
rootView.addView(firstChild)
|
||||||
|
val firstGrandChild = View(mContext)
|
||||||
|
firstChild.addView(firstGrandChild)
|
||||||
|
val secondGrandChild = View(mContext)
|
||||||
|
firstChild.addView(secondGrandChild)
|
||||||
|
val secondChild = View(mContext)
|
||||||
|
rootView.addView(secondChild)
|
||||||
|
|
||||||
|
val childParams = LinearLayout.LayoutParams(
|
||||||
|
0 /* width */,
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
childParams.weight = 0.5f
|
||||||
|
firstChild.layoutParams = childParams
|
||||||
|
secondChild.layoutParams = childParams
|
||||||
|
firstGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
|
||||||
|
(firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
|
||||||
|
.addRule(RelativeLayout.ALIGN_PARENT_START)
|
||||||
|
(firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
|
||||||
|
.addRule(RelativeLayout.ALIGN_PARENT_TOP)
|
||||||
|
secondGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
|
||||||
|
(secondGrandChild.layoutParams as RelativeLayout.LayoutParams)
|
||||||
|
.addRule(RelativeLayout.ALIGN_PARENT_END)
|
||||||
|
(secondGrandChild.layoutParams as RelativeLayout.LayoutParams)
|
||||||
|
.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
|
||||||
|
|
||||||
|
forceLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forceLayout() {
|
||||||
|
rootView.measure(
|
||||||
|
View.MeasureSpec.makeMeasureSpec(200 /* width */, View.MeasureSpec.AT_MOST),
|
||||||
|
View.MeasureSpec.makeMeasureSpec(100 /* height */, View.MeasureSpec.AT_MOST)
|
||||||
|
)
|
||||||
|
rootView.layout(0 /* l */, 0 /* t */, 200 /* r */, 100 /* b */)
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkBounds(v: View, l: Int, t: Int, r: Int, b: Int) {
|
private fun checkBounds(v: View, l: Int, t: Int, r: Int, b: Int) {
|
||||||
assertEquals(l, v.left)
|
assertEquals(l, v.left)
|
||||||
assertEquals(t, v.top)
|
assertEquals(t, v.top)
|
||||||
|
|||||||
Reference in New Issue
Block a user