Merge "Create a ViewBoundAnimator to animate view layout updates." into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
bc4b6762fa
@@ -15,5 +15,14 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<!-- DialogLaunchAnimator -->
|
||||
<item type="id" name="launch_animation_running"/>
|
||||
|
||||
<!-- ViewBoundsAnimator -->
|
||||
<item type="id" name="tag_animator"/>
|
||||
<item type="id" name="tag_layout_listener"/>
|
||||
<item type="id" name="tag_override_bottom"/>
|
||||
<item type="id" name="tag_override_left"/>
|
||||
<item type="id" name="tag_override_right"/>
|
||||
<item type="id" name="tag_override_top"/>
|
||||
</resources>
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.animation
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.util.IntProperty
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Interpolator
|
||||
|
||||
/**
|
||||
* A class that allows changes in bounds within a view hierarchy to animate seamlessly between the
|
||||
* start and end state.
|
||||
*/
|
||||
class ViewBoundAnimator {
|
||||
// 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_BOUNDS = setOf(Bound.LEFT, Bound.TOP, Bound.RIGHT, Bound.BOTTOM)
|
||||
|
||||
/** The properties used to animate the view bounds. */
|
||||
private val PROPERTIES = mapOf(
|
||||
Bound.LEFT to createViewProperty(Bound.LEFT),
|
||||
Bound.TOP to createViewProperty(Bound.TOP),
|
||||
Bound.RIGHT to createViewProperty(Bound.RIGHT),
|
||||
Bound.BOTTOM to createViewProperty(Bound.BOTTOM)
|
||||
)
|
||||
|
||||
private fun createViewProperty(bound: Bound): IntProperty<View> {
|
||||
return object : IntProperty<View>(bound.label) {
|
||||
override fun setValue(view: View, value: Int) {
|
||||
setBound(view, bound, value)
|
||||
}
|
||||
|
||||
override fun get(view: View): Int {
|
||||
return getBound(view, bound) ?: bound.getValue(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the animator to watch for changes to the layout of [rootView] and its children
|
||||
* and animate them. The animation can be limited to a subset of [bounds]. It uses the
|
||||
* given [interpolator] and [duration].
|
||||
*
|
||||
* If a new layout change happens while an animation is already in progress, the animation
|
||||
* is updated to continue from the current values to the new end state.
|
||||
*
|
||||
* The animator continues to respond to layout changes until [stopAnimating] is called.
|
||||
*
|
||||
* Successive calls to this method override the previous settings ([interpolator] and
|
||||
* [duration]). The changes take effect on the next animation.
|
||||
*
|
||||
* TODO(b/221418522): remove the ability to select which bounds to animate and always
|
||||
* animate all of them.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun animate(
|
||||
rootView: View,
|
||||
bounds: Set<Bound> = DEFAULT_BOUNDS,
|
||||
interpolator: Interpolator = DEFAULT_INTERPOLATOR,
|
||||
duration: Long = DEFAULT_DURATION
|
||||
) {
|
||||
animate(rootView, bounds, interpolator, duration, false /* ephemeral */)
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [animate], but only takes effect on the next layout update, then unregisters itself
|
||||
* once the first animation is complete.
|
||||
*
|
||||
* TODO(b/221418522): remove the ability to select which bounds to animate and always
|
||||
* animate all of them.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun animateNextUpdate(
|
||||
rootView: View,
|
||||
bounds: Set<Bound> = DEFAULT_BOUNDS,
|
||||
interpolator: Interpolator = DEFAULT_INTERPOLATOR,
|
||||
duration: Long = DEFAULT_DURATION
|
||||
) {
|
||||
animate(rootView, bounds, interpolator, duration, true /* ephemeral */)
|
||||
}
|
||||
|
||||
private fun animate(
|
||||
rootView: View,
|
||||
bounds: Set<Bound>,
|
||||
interpolator: Interpolator,
|
||||
duration: Long,
|
||||
ephemeral: Boolean
|
||||
) {
|
||||
val listener = createListener(bounds, interpolator, duration, ephemeral)
|
||||
recursivelyAddListener(rootView, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the animator to stop watching for changes to the layout of [rootView] and its
|
||||
* children.
|
||||
*
|
||||
* Any animations already in progress continue until their natural conclusion.
|
||||
*/
|
||||
fun stopAnimating(rootView: View) {
|
||||
val listener = rootView.getTag(R.id.tag_layout_listener)
|
||||
if (listener != null && listener is View.OnLayoutChangeListener) {
|
||||
rootView.setTag(R.id.tag_layout_listener, null /* tag */)
|
||||
rootView.removeOnLayoutChangeListener(listener)
|
||||
}
|
||||
|
||||
if (rootView is ViewGroup) {
|
||||
for (i in 0 until rootView.childCount) {
|
||||
stopAnimating(rootView.getChildAt(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createListener(
|
||||
bounds: Set<Bound>,
|
||||
interpolator: Interpolator,
|
||||
duration: Long,
|
||||
ephemeral: Boolean
|
||||
): View.OnLayoutChangeListener {
|
||||
return object : View.OnLayoutChangeListener {
|
||||
override fun onLayoutChange(
|
||||
view: View?,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: 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
|
||||
|
||||
(view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
|
||||
|
||||
if (view.visibility == View.GONE || view.visibility == View.INVISIBLE) {
|
||||
setBound(view, Bound.LEFT, left)
|
||||
setBound(view, Bound.TOP, top)
|
||||
setBound(view, Bound.RIGHT, right)
|
||||
setBound(view, Bound.BOTTOM, bottom)
|
||||
return
|
||||
}
|
||||
|
||||
val startValues = mapOf(
|
||||
Bound.LEFT to startLeft,
|
||||
Bound.TOP to startTop,
|
||||
Bound.RIGHT to startRight,
|
||||
Bound.BOTTOM to startBottom
|
||||
)
|
||||
val endValues = mapOf(
|
||||
Bound.LEFT to left,
|
||||
Bound.TOP to top,
|
||||
Bound.RIGHT to right,
|
||||
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)
|
||||
|
||||
if (boundsToAnimate.isNotEmpty()) {
|
||||
startAnimation(
|
||||
view,
|
||||
boundsToAnimate,
|
||||
startValues,
|
||||
endValues,
|
||||
interpolator,
|
||||
duration,
|
||||
ephemeral
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
view.addOnLayoutChangeListener(listener)
|
||||
view.setTag(R.id.tag_layout_listener, listener)
|
||||
if (view is ViewGroup) {
|
||||
for (i in 0 until view.childCount) {
|
||||
recursivelyAddListener(view.getChildAt(i), listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBound(view: View, bound: Bound): Int? {
|
||||
return view.getTag(bound.overrideTag) as? Int
|
||||
}
|
||||
|
||||
private fun setBound(view: View, bound: Bound, value: Int) {
|
||||
view.setTag(bound.overrideTag, value)
|
||||
bound.setValue(view, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private fun startAnimation(
|
||||
view: View,
|
||||
bounds: Set<Bound>,
|
||||
startValues: Map<Bound, Int>,
|
||||
endValues: Map<Bound, Int>,
|
||||
interpolator: Interpolator,
|
||||
duration: Long,
|
||||
ephemeral: Boolean
|
||||
) {
|
||||
val propertyValuesHolders = buildList {
|
||||
bounds.forEach { bound ->
|
||||
add(
|
||||
PropertyValuesHolder.ofInt(
|
||||
PROPERTIES[bound],
|
||||
startValues.getValue(bound),
|
||||
endValues.getValue(bound)
|
||||
)
|
||||
)
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
|
||||
animator.interpolator = interpolator
|
||||
animator.duration = duration
|
||||
animator.addListener(object : AnimatorListenerAdapter() {
|
||||
var cancelled = false
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
view.setTag(R.id.tag_animator, null /* tag */)
|
||||
bounds.forEach { view.setTag(it.overrideTag, null /* tag */) }
|
||||
|
||||
// When an animation is cancelled, a new one might be taking over. We shouldn't
|
||||
// unregister the listener yet.
|
||||
if (ephemeral && !cancelled) {
|
||||
val listener = view.getTag(R.id.tag_layout_listener)
|
||||
if (listener != null && listener is View.OnLayoutChangeListener) {
|
||||
view.setTag(R.id.tag_layout_listener, null /* tag */)
|
||||
view.removeOnLayoutChangeListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator?) {
|
||||
cancelled = true
|
||||
}
|
||||
})
|
||||
|
||||
bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) }
|
||||
|
||||
view.setTag(R.id.tag_animator, animator)
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.android.systemui.animation
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import junit.framework.Assert.assertEquals
|
||||
import junit.framework.Assert.assertNotNull
|
||||
import junit.framework.Assert.assertNull
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
class ViewBoundAnimatorTest : SysuiTestCase() {
|
||||
companion object {
|
||||
private const val TEST_DURATION = 1000L
|
||||
private val TEST_INTERPOLATOR = Interpolators.LINEAR
|
||||
}
|
||||
|
||||
private lateinit var rootView: LinearLayout
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
rootView = LinearLayout(mContext)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
ViewBoundAnimator.stopAnimating(rootView)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun respectsAnimationParameters() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animate(
|
||||
rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
|
||||
)
|
||||
rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
val animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
|
||||
assertEquals(animator.interpolator, TEST_INTERPOLATOR)
|
||||
assertEquals(animator.duration, TEST_DURATION)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animatesFromStartToEnd() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animate(rootView)
|
||||
// Change all bounds.
|
||||
rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)
|
||||
|
||||
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)
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
// The end values should be those of the latest layout.
|
||||
checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animatesSuccessiveLayoutChanges() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animate(rootView)
|
||||
// Change all bounds.
|
||||
rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)
|
||||
|
||||
// Change only top and right.
|
||||
rootView.layout(0 /* l */, 20 /* t */, 60 /* r */, 80 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 0, t = 20, r = 60, b = 80)
|
||||
|
||||
// Change all bounds again.
|
||||
rootView.layout(5 /* l */, 25 /* t */, 55 /* r */, 95 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 5, t = 25, r = 55, b = 95)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animatesFromPreviousAnimationProgress() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animateNextUpdate(rootView, interpolator = TEST_INTERPOLATOR)
|
||||
// Change all bounds.
|
||||
rootView.layout(0 /* l */, 20 /* t */, 70 /* r */, 80 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
advanceAnimation(rootView, fraction = 0.5f)
|
||||
checkBounds(rootView, l = 5, t = 15, r = 60, b = 65)
|
||||
|
||||
// Change all bounds again.
|
||||
rootView.layout(25 /* l */, 25 /* t */, 55 /* r */, 60 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 5, t = 15, r = 60, b = 65)
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 25, t = 25, r = 55, b = 60)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animatesRootAndChildren() {
|
||||
val firstChild = View(mContext)
|
||||
rootView.addView(firstChild)
|
||||
val secondChild = View(mContext)
|
||||
rootView.addView(secondChild)
|
||||
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 */)
|
||||
|
||||
ViewBoundAnimator.animate(rootView)
|
||||
// Change all bounds.
|
||||
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 */)
|
||||
|
||||
assertNotNull(rootView.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)
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.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)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotAnimateInvisibleViews() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animate(rootView)
|
||||
// GONE.
|
||||
rootView.visibility = View.GONE
|
||||
rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)
|
||||
|
||||
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 */)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotAnimateUnchangingBounds() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animate(rootView)
|
||||
// No bounds are changed.
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
|
||||
|
||||
// Change only right and bottom.
|
||||
rootView.layout(10 /* l */, 10 /* t */, 70 /* r */, 80 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 10, t = 10, r = 70, b = 80)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotAnimateExcludedBounds() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animate(
|
||||
rootView,
|
||||
bounds = setOf(ViewBoundAnimator.Bound.LEFT, ViewBoundAnimator.Bound.TOP),
|
||||
interpolator = TEST_INTERPOLATOR
|
||||
)
|
||||
// Change all bounds.
|
||||
rootView.layout(0 /* l */, 20 /* t */, 70 /* r */, 80 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
advanceAnimation(rootView, 0.5f)
|
||||
checkBounds(rootView, l = 5, t = 15, r = 70, b = 80)
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 0, t = 20, r = 70, b = 80)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stopsAnimatingAfterSingleLayout() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animateNextUpdate(rootView)
|
||||
// Change all bounds.
|
||||
rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)
|
||||
|
||||
// Change all bounds again.
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50/* r */, 50 /* b */)
|
||||
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stopsAnimatingWhenInstructed() {
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
|
||||
|
||||
ViewBoundAnimator.animate(rootView)
|
||||
// Change all bounds.
|
||||
rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)
|
||||
|
||||
assertNotNull(rootView.getTag(R.id.tag_animator))
|
||||
endAnimation(rootView)
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)
|
||||
|
||||
ViewBoundAnimator.stopAnimating(rootView)
|
||||
// Change all bounds again.
|
||||
rootView.layout(10 /* l */, 10 /* t */, 50/* r */, 50 /* b */)
|
||||
|
||||
assertNull(rootView.getTag(R.id.tag_animator))
|
||||
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
|
||||
}
|
||||
|
||||
private fun checkBounds(v: View, l: Int, t: Int, r: Int, b: Int) {
|
||||
assertEquals(l, v.left)
|
||||
assertEquals(t, v.top)
|
||||
assertEquals(r, v.right)
|
||||
assertEquals(b, v.bottom)
|
||||
}
|
||||
|
||||
private fun advanceAnimation(rootView: View, fraction: Float) {
|
||||
(rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.setCurrentFraction(fraction)
|
||||
|
||||
if (rootView is ViewGroup) {
|
||||
for (i in 0 until rootView.childCount) {
|
||||
advanceAnimation(rootView.getChildAt(i), fraction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun endAnimation(rootView: View) {
|
||||
(rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.end()
|
||||
|
||||
if (rootView is ViewGroup) {
|
||||
for (i in 0 until rootView.childCount) {
|
||||
endAnimation(rootView.getChildAt(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user