Adds 'flinging-then-springing' to the PhysicsAnimator.
See http://go/physics-animator-design#heading=h.ib2uxhal5wrq and http://go/physics-animator-design#heading=h.fu8468xbcwxo Test: atest SystemUITests Change-Id: I55b0db822c9468de239dc5d2aa9fa080ad7131ab
This commit is contained in:
@@ -27,6 +27,9 @@ import androidx.dynamicanimation.animation.SpringAnimation
|
||||
import androidx.dynamicanimation.animation.SpringForce
|
||||
import com.android.systemui.util.animation.PhysicsAnimator.Companion.getInstance
|
||||
import java.util.WeakHashMap
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Extension function for all objects which will return a PhysicsAnimator instance for that object.
|
||||
@@ -35,6 +38,15 @@ val <T : View> T.physicsAnimator: PhysicsAnimator<T> get() { return getInstance(
|
||||
|
||||
private const val TAG = "PhysicsAnimator"
|
||||
|
||||
private val UNSET = -Float.MAX_VALUE
|
||||
|
||||
/**
|
||||
* [FlingAnimation] multiplies the friction set via [FlingAnimation.setFriction] by 4.2f, which is
|
||||
* where this number comes from. We use it in [PhysicsAnimator.flingThenSpring] to calculate the
|
||||
* minimum velocity for a fling to reach a certain value, given the fling's friction.
|
||||
*/
|
||||
private const val FLING_FRICTION_SCALAR_MULTIPLIER = 4.2f
|
||||
|
||||
typealias EndAction = () -> Unit
|
||||
|
||||
/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */
|
||||
@@ -235,6 +247,71 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
return fling(property, startVelocity, config.friction, config.min, config.max)
|
||||
}
|
||||
|
||||
/**
|
||||
* Flings a property using the given start velocity. If the fling animation reaches the min/max
|
||||
* bounds (from the [flingConfig]) with velocity remaining, it'll overshoot it and spring back.
|
||||
*
|
||||
* If the object is already out of the fling bounds, it will immediately spring back within
|
||||
* bounds.
|
||||
*
|
||||
* This is useful for animating objects that are bounded by constraints such as screen edges,
|
||||
* since otherwise the fling animation would end abruptly upon reaching the min/max bounds.
|
||||
*
|
||||
* @param property The property to animate.
|
||||
* @param startVelocity The velocity, in pixels/second, with which to start the fling. If the
|
||||
* object is already outside the fling bounds, this velocity will be used as the start velocity
|
||||
* of the spring that will spring it back within bounds.
|
||||
* @param flingMustReachMinOrMax If true, the fling animation is guaranteed to reach either its
|
||||
* minimum bound (if [startVelocity] is negative) or maximum bound (if it's positive). The
|
||||
* animator will use startVelocity if it's sufficient, or add more velocity if necessary. This
|
||||
* is useful when fling's deceleration-based physics are preferable to the acceleration-based
|
||||
* forces used by springs - typically, when you're allowing the user to move an object somewhere
|
||||
* on the screen, but it needs to be along an edge.
|
||||
* @param flingConfig The configuration to use for the fling portion of the animation.
|
||||
* @param springConfig The configuration to use for the spring portion of the animation.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun flingThenSpring(
|
||||
property: FloatPropertyCompat<in T>,
|
||||
startVelocity: Float,
|
||||
flingConfig: FlingConfig,
|
||||
springConfig: SpringConfig,
|
||||
flingMustReachMinOrMax: Boolean = false
|
||||
): PhysicsAnimator<T> {
|
||||
val flingConfigCopy = flingConfig.copy()
|
||||
val springConfigCopy = springConfig.copy()
|
||||
val toAtLeast = if (startVelocity < 0) flingConfig.min else flingConfig.max
|
||||
|
||||
// If the fling needs to reach min/max, calculate the velocity required to do so and use
|
||||
// that if the provided start velocity is not sufficient.
|
||||
if (flingMustReachMinOrMax &&
|
||||
toAtLeast != -Float.MAX_VALUE && toAtLeast != Float.MAX_VALUE) {
|
||||
val distanceToDestination = toAtLeast - property.getValue(target)
|
||||
|
||||
// The minimum velocity required for the fling to end up at the given destination,
|
||||
// taking the provided fling friction value.
|
||||
val velocityToReachDestination = distanceToDestination *
|
||||
(flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
|
||||
|
||||
// Try to use the provided start velocity, but use the required velocity to reach the
|
||||
// destination if the provided velocity is insufficient.
|
||||
val sufficientVelocity =
|
||||
if (distanceToDestination < 0)
|
||||
min(velocityToReachDestination, startVelocity)
|
||||
else
|
||||
max(velocityToReachDestination, startVelocity)
|
||||
|
||||
flingConfigCopy.startVelocity = sufficientVelocity
|
||||
springConfigCopy.finalPosition = toAtLeast
|
||||
} else {
|
||||
flingConfigCopy.startVelocity = startVelocity
|
||||
}
|
||||
|
||||
flingConfigs[property] = flingConfigCopy
|
||||
springConfigs[property] = springConfigCopy
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener that will be called whenever any property on the animated object is updated.
|
||||
* This will be called on every animation frame, with the current value of the animated object
|
||||
@@ -246,7 +323,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener that will be called whenever a property's animation ends. This is useful if
|
||||
* Adds a listener that will be called when a property stops animating. This is useful if
|
||||
* you care about a specific property ending, or want to use the end value/end velocity from a
|
||||
* particular property's animation. If you just want to run an action when all property
|
||||
* animations have ended, use [withEndActions].
|
||||
@@ -311,6 +388,114 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
"your test setup.")
|
||||
}
|
||||
|
||||
// Functions that will actually start the animations. These are run after we build and add
|
||||
// the InternalListener, since some animations might update/end immediately and we don't
|
||||
// want to miss those updates.
|
||||
val animationStartActions = ArrayList<() -> Unit>()
|
||||
|
||||
for (animatedProperty in getAnimatedProperties()) {
|
||||
val flingConfig = flingConfigs[animatedProperty]
|
||||
val springConfig = springConfigs[animatedProperty]
|
||||
|
||||
// The property's current value on the object.
|
||||
val currentValue = animatedProperty.getValue(target)
|
||||
|
||||
// Start by checking for a fling configuration. If one is present, we're either flinging
|
||||
// or flinging-then-springing. Either way, we'll want to start the fling first.
|
||||
if (flingConfig != null) {
|
||||
animationStartActions.add {
|
||||
// When the animation is starting, adjust the min/max bounds to include the
|
||||
// current value of the property, if necessary. This is required to allow a
|
||||
// fling to bring an out-of-bounds object back into bounds. For example, if an
|
||||
// object was dragged halfway off the left side of the screen, but then flung to
|
||||
// the right, we don't want the animation to end instantly just because the
|
||||
// object started out of bounds. If the fling is in the direction that would
|
||||
// take it farther out of bounds, it will end instantly as expected.
|
||||
flingConfig.apply {
|
||||
min = min(currentValue, this.min)
|
||||
max = max(currentValue, this.max)
|
||||
}
|
||||
|
||||
// Apply the configuration and start the animation.
|
||||
getFlingAnimation(animatedProperty)
|
||||
.also { flingConfig.applyToAnimation(it) }
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a spring configuration. If one is present, we're either springing, or
|
||||
// flinging-then-springing.
|
||||
if (springConfig != null) {
|
||||
|
||||
// If there is no corresponding fling config, we're only springing.
|
||||
if (flingConfig == null) {
|
||||
// Apply the configuration and start the animation.
|
||||
val springAnim = getSpringAnimation(animatedProperty)
|
||||
springConfig.applyToAnimation(springAnim)
|
||||
animationStartActions.add(springAnim::start)
|
||||
} else {
|
||||
// If there's a corresponding fling config, we're flinging-then-springing. Save
|
||||
// the fling's original bounds so we can spring to them when the fling ends.
|
||||
val flingMin = flingConfig.min
|
||||
val flingMax = flingConfig.max
|
||||
|
||||
// Add an end listener that will start the spring when the fling ends.
|
||||
endListeners.add(0, object : EndListener<T> {
|
||||
override fun onAnimationEnd(
|
||||
target: T,
|
||||
property: FloatPropertyCompat<in T>,
|
||||
wasFling: Boolean,
|
||||
canceled: Boolean,
|
||||
finalValue: Float,
|
||||
finalVelocity: Float,
|
||||
allRelevantPropertyAnimsEnded: Boolean
|
||||
) {
|
||||
// If this isn't the relevant property, it wasn't a fling, or the fling
|
||||
// was explicitly cancelled, don't spring.
|
||||
if (property != animatedProperty || !wasFling || canceled) {
|
||||
return
|
||||
}
|
||||
|
||||
val endedWithVelocity = abs(finalVelocity) > 0
|
||||
|
||||
// If the object was out of bounds when the fling animation started, it
|
||||
// will immediately end. In that case, we'll spring it back in bounds.
|
||||
val endedOutOfBounds = finalValue !in flingMin..flingMax
|
||||
|
||||
// If the fling ended either out of bounds or with remaining velocity,
|
||||
// it's time to spring.
|
||||
if (endedWithVelocity || endedOutOfBounds) {
|
||||
springConfig.startVelocity = finalVelocity
|
||||
|
||||
// If the spring's final position isn't set, this is a
|
||||
// flingThenSpring where flingMustReachMinOrMax was false. We'll
|
||||
// need to set the spring's final position here.
|
||||
if (springConfig.finalPosition == UNSET) {
|
||||
if (endedWithVelocity) {
|
||||
// If the fling ended with negative velocity, that means it
|
||||
// hit the min bound, so spring to that bound (and vice
|
||||
// versa).
|
||||
springConfig.finalPosition =
|
||||
if (finalVelocity < 0) flingMin else flingMax
|
||||
} else if (endedOutOfBounds) {
|
||||
// If the fling ended out of bounds, spring it to the
|
||||
// nearest bound.
|
||||
springConfig.finalPosition =
|
||||
if (finalValue < flingMin) flingMin else flingMax
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the configuration and start the spring animation.
|
||||
getSpringAnimation(animatedProperty)
|
||||
.also { springConfig.applyToAnimation(it) }
|
||||
.start()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add an internal listener that will dispatch animation events to the provided listeners.
|
||||
internalListeners.add(InternalListener(
|
||||
getAnimatedProperties(),
|
||||
@@ -318,24 +503,10 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
ArrayList(endListeners),
|
||||
ArrayList(endActions)))
|
||||
|
||||
for ((property, config) in flingConfigs) {
|
||||
val currentValue = property.getValue(target)
|
||||
|
||||
// If the fling is already out of bounds, don't start it.
|
||||
if (currentValue <= config.min || currentValue >= config.max) {
|
||||
continue
|
||||
}
|
||||
|
||||
val flingAnim = getFlingAnimation(property)
|
||||
config.applyToAnimation(flingAnim)
|
||||
flingAnim.start()
|
||||
}
|
||||
|
||||
for ((property, config) in springConfigs) {
|
||||
val springAnim = getSpringAnimation(property)
|
||||
config.applyToAnimation(springAnim)
|
||||
springAnim.start()
|
||||
}
|
||||
// Actually start the DynamicAnimations. This is delayed until after the InternalListener is
|
||||
// constructed and added so that we don't miss the end listener firing for any animations
|
||||
// that immediately end.
|
||||
animationStartActions.forEach { it.invoke() }
|
||||
|
||||
clearAnimator()
|
||||
}
|
||||
@@ -381,7 +552,10 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
}
|
||||
anim.addEndListener { _, canceled, value, velocity ->
|
||||
internalListeners.removeAll {
|
||||
it.onInternalAnimationEnd(property, canceled, value, velocity) } }
|
||||
it.onInternalAnimationEnd(
|
||||
property, canceled, value, velocity, anim is FlingAnimation)
|
||||
}
|
||||
}
|
||||
return anim
|
||||
}
|
||||
|
||||
@@ -434,7 +608,8 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
property: FloatPropertyCompat<in T>,
|
||||
canceled: Boolean,
|
||||
finalValue: Float,
|
||||
finalVelocity: Float
|
||||
finalVelocity: Float,
|
||||
isFling: Boolean
|
||||
): Boolean {
|
||||
|
||||
// If this property animation isn't relevant to this listener, ignore it.
|
||||
@@ -461,7 +636,15 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
|
||||
val allEnded = !arePropertiesAnimating(properties)
|
||||
endListeners.forEach {
|
||||
it.onAnimationEnd(target, property, canceled, finalValue, finalVelocity, allEnded) }
|
||||
it.onAnimationEnd(
|
||||
target, property, isFling, canceled, finalValue, finalVelocity,
|
||||
allEnded)
|
||||
|
||||
// Check that the end listener didn't restart this property's animation.
|
||||
if (isPropertyAnimating(property)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If all of the animations that this listener cares about have ended, run the end
|
||||
// actions unless the animation was canceled.
|
||||
@@ -524,15 +707,15 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
data class SpringConfig internal constructor(
|
||||
internal var stiffness: Float,
|
||||
internal var dampingRatio: Float,
|
||||
internal var startVel: Float = 0f,
|
||||
internal var finalPosition: Float = -Float.MAX_VALUE
|
||||
internal var startVelocity: Float = 0f,
|
||||
internal var finalPosition: Float = UNSET
|
||||
) {
|
||||
|
||||
constructor() :
|
||||
this(defaultSpring.stiffness, defaultSpring.dampingRatio)
|
||||
|
||||
constructor(stiffness: Float, dampingRatio: Float) :
|
||||
this(stiffness = stiffness, dampingRatio = dampingRatio, startVel = 0f)
|
||||
this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f)
|
||||
|
||||
/** Apply these configuration settings to the given SpringAnimation. */
|
||||
internal fun applyToAnimation(anim: SpringAnimation) {
|
||||
@@ -543,7 +726,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
finalPosition = this@SpringConfig.finalPosition
|
||||
}
|
||||
|
||||
if (startVel != 0f) anim.setStartVelocity(startVel)
|
||||
if (startVelocity != 0f) anim.setStartVelocity(startVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,7 +739,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
internal var friction: Float,
|
||||
internal var min: Float,
|
||||
internal var max: Float,
|
||||
internal var startVel: Float
|
||||
internal var startVelocity: Float
|
||||
) {
|
||||
|
||||
constructor() : this(defaultFling.friction)
|
||||
@@ -565,7 +748,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
this(friction, defaultFling.min, defaultFling.max)
|
||||
|
||||
constructor(friction: Float, min: Float, max: Float) :
|
||||
this(friction, min, max, startVel = 0f)
|
||||
this(friction, min, max, startVelocity = 0f)
|
||||
|
||||
/** Apply these configuration settings to the given FlingAnimation. */
|
||||
internal fun applyToAnimation(anim: FlingAnimation) {
|
||||
@@ -573,7 +756,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
friction = this@FlingConfig.friction
|
||||
setMinValue(min)
|
||||
setMaxValue(max)
|
||||
setStartVelocity(startVel)
|
||||
setStartVelocity(startVelocity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,6 +809,10 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
*
|
||||
* @param target The animated object itself.
|
||||
* @param property The property whose animation has just ended.
|
||||
* @param wasFling Whether this property ended after a fling animation (as opposed to a
|
||||
* spring animation). If this property was animated via [flingThenSpring], this will be true
|
||||
* if the fling animation did not reach the min/max bounds, decelerating to a stop
|
||||
* naturally. It will be false if it hit the bounds and was sprung back.
|
||||
* @param canceled Whether the animation was explicitly canceled before it naturally ended.
|
||||
* @param finalValue The final value of the animated property.
|
||||
* @param finalVelocity The final velocity (in pixels per second) of the ended animation.
|
||||
@@ -663,6 +850,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
|
||||
fun onAnimationEnd(
|
||||
target: T,
|
||||
property: FloatPropertyCompat<in T>,
|
||||
wasFling: Boolean,
|
||||
canceled: Boolean,
|
||||
finalValue: Float,
|
||||
finalVelocity: Float,
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.ArrayMap
|
||||
import androidx.dynamicanimation.animation.FloatPropertyCompat
|
||||
import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.prepareForTest
|
||||
import java.util.ArrayDeque
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -119,6 +120,7 @@ object PhysicsAnimatorTestUtils {
|
||||
override fun onAnimationEnd(
|
||||
target: T,
|
||||
property: FloatPropertyCompat<in T>,
|
||||
wasFling: Boolean,
|
||||
canceled: Boolean,
|
||||
finalValue: Float,
|
||||
finalVelocity: Float,
|
||||
@@ -389,8 +391,6 @@ object PhysicsAnimatorTestUtils {
|
||||
val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1)
|
||||
|
||||
animationThreadHandler.post {
|
||||
val animatedProperties = animator.getAnimatedProperties()
|
||||
|
||||
// Add an update listener that dispatches to any test update listeners added by
|
||||
// tests.
|
||||
animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> {
|
||||
@@ -398,6 +398,10 @@ object PhysicsAnimatorTestUtils {
|
||||
target: T,
|
||||
values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
|
||||
) {
|
||||
values.forEach { (property, value) ->
|
||||
allUpdates.getOrPut(property, { ArrayList() }).add(value)
|
||||
}
|
||||
|
||||
for (listener in testUpdateListeners) {
|
||||
listener.onAnimationUpdateForProperty(target, values)
|
||||
}
|
||||
@@ -410,6 +414,7 @@ object PhysicsAnimatorTestUtils {
|
||||
override fun onAnimationEnd(
|
||||
target: T,
|
||||
property: FloatPropertyCompat<in T>,
|
||||
wasFling: Boolean,
|
||||
canceled: Boolean,
|
||||
finalValue: Float,
|
||||
finalVelocity: Float,
|
||||
@@ -417,7 +422,7 @@ object PhysicsAnimatorTestUtils {
|
||||
) {
|
||||
for (listener in testEndListeners) {
|
||||
listener.onAnimationEnd(
|
||||
target, property, canceled, finalValue, finalVelocity,
|
||||
target, property, wasFling, canceled, finalValue, finalVelocity,
|
||||
allRelevantPropertyAnimsEnded)
|
||||
}
|
||||
|
||||
@@ -432,31 +437,6 @@ object PhysicsAnimatorTestUtils {
|
||||
}
|
||||
})
|
||||
|
||||
val updateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>().also {
|
||||
it.add(object : PhysicsAnimator.UpdateListener<T> {
|
||||
override fun onAnimationUpdateForProperty(
|
||||
target: T,
|
||||
values: ArrayMap<FloatPropertyCompat<in T>,
|
||||
PhysicsAnimator.AnimationUpdate>
|
||||
) {
|
||||
values.forEach { (property, value) ->
|
||||
allUpdates.getOrPut(property, { ArrayList() }).add(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an internal listener at the head of the list that captures update values
|
||||
* directly from DynamicAnimation. We use this to build a list of all updates so we
|
||||
* can verify that InternalListener dispatches to the real listeners properly.
|
||||
*/
|
||||
animator.internalListeners.add(0, animator.InternalListener(
|
||||
animatedProperties,
|
||||
updateListeners,
|
||||
ArrayList(),
|
||||
ArrayList()))
|
||||
|
||||
animator.startInternal()
|
||||
unblockLatch.countDown()
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ class PhysicsAnimatorTest : SysuiTestCase() {
|
||||
verify(mockEndListener).onAnimationEnd(
|
||||
testView,
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
wasFling = false,
|
||||
canceled = false,
|
||||
finalValue = 10f,
|
||||
finalVelocity = 0f,
|
||||
@@ -176,6 +177,7 @@ class PhysicsAnimatorTest : SysuiTestCase() {
|
||||
verify(mockEndListener).onAnimationEnd(
|
||||
testView,
|
||||
DynamicAnimation.TRANSLATION_Y,
|
||||
wasFling = false,
|
||||
canceled = false,
|
||||
finalValue = 500f,
|
||||
finalVelocity = 0f,
|
||||
@@ -215,8 +217,8 @@ class PhysicsAnimatorTest : SysuiTestCase() {
|
||||
|
||||
verifyUpdateListenerCalls(animator, mockUpdateListener)
|
||||
verify(mockEndListener, times(1)).onAnimationEnd(
|
||||
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), anyFloat(), anyFloat(),
|
||||
eq(true))
|
||||
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(false), anyFloat(),
|
||||
anyFloat(), eq(true))
|
||||
verify(mockEndAction, times(1)).run()
|
||||
|
||||
animator
|
||||
@@ -330,13 +332,24 @@ class PhysicsAnimatorTest : SysuiTestCase() {
|
||||
// The original end listener should also have been called, with allEnded = true since it was
|
||||
// provided to an animator that animated only TRANSLATION_X.
|
||||
verify(mockEndListener, times(1))
|
||||
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, false, 200f, 0f, true)
|
||||
.onAnimationEnd(
|
||||
testView, DynamicAnimation.TRANSLATION_X,
|
||||
wasFling = false,
|
||||
canceled = false,
|
||||
finalValue = 200f,
|
||||
finalVelocity = 0f,
|
||||
allRelevantPropertyAnimsEnded = true)
|
||||
verifyNoMoreInteractions(mockEndListener)
|
||||
|
||||
// The second end listener should have been called, but with allEnded = false since it was
|
||||
// provided to an animator that animated both TRANSLATION_X and TRANSLATION_Y.
|
||||
verify(secondEndListener, times(1))
|
||||
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, false, 200f, 0f, false)
|
||||
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X,
|
||||
wasFling = false,
|
||||
canceled = false,
|
||||
finalValue = 200f,
|
||||
finalVelocity = 0f,
|
||||
allRelevantPropertyAnimsEnded = false)
|
||||
verifyNoMoreInteractions(secondEndListener)
|
||||
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y)
|
||||
@@ -346,7 +359,12 @@ class PhysicsAnimatorTest : SysuiTestCase() {
|
||||
verifyNoMoreInteractions(mockEndListener)
|
||||
|
||||
verify(secondEndListener, times(1))
|
||||
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y, false, 4000f, 0f, true)
|
||||
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y,
|
||||
wasFling = false,
|
||||
canceled = false,
|
||||
finalValue = 4000f,
|
||||
finalVelocity = 0f,
|
||||
allRelevantPropertyAnimsEnded = true)
|
||||
verifyNoMoreInteractions(secondEndListener)
|
||||
}
|
||||
|
||||
@@ -365,8 +383,8 @@ class PhysicsAnimatorTest : SysuiTestCase() {
|
||||
assertEquals(10f, testView.translationX, 1f)
|
||||
verify(mockEndListener, times(1))
|
||||
.onAnimationEnd(
|
||||
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(10f),
|
||||
anyFloat(), eq(true))
|
||||
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false),
|
||||
eq(10f), anyFloat(), eq(true))
|
||||
|
||||
animator
|
||||
.fling(
|
||||
@@ -382,8 +400,8 @@ class PhysicsAnimatorTest : SysuiTestCase() {
|
||||
assertEquals(-5f, testView.translationX, 1f)
|
||||
verify(mockEndListener, times(1))
|
||||
.onAnimationEnd(
|
||||
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(-5f),
|
||||
anyFloat(), eq(true))
|
||||
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false),
|
||||
eq(-5f), anyFloat(), eq(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -427,6 +445,118 @@ class PhysicsAnimatorTest : SysuiTestCase() {
|
||||
assertEquals(200f, testView.translationX, 1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFlingThenSpring() {
|
||||
PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
|
||||
|
||||
// Start at 500f and fling hard to the left. We should quickly reach the 250f minimum, fly
|
||||
// past it since there's so much velocity remaining, then spring back to 250f.
|
||||
testView.translationX = 500f
|
||||
animator
|
||||
.flingThenSpring(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
-5000f,
|
||||
flingConfig.apply { min = 250f },
|
||||
springConfig)
|
||||
.addUpdateListener(mockUpdateListener)
|
||||
.addEndListener(mockEndListener)
|
||||
.withEndActions(mockEndAction::run)
|
||||
.start()
|
||||
|
||||
// Block until we pass the minimum.
|
||||
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(
|
||||
animator) { v -> v.translationX <= 250f }
|
||||
|
||||
// Double check that the view is there.
|
||||
assertTrue(testView.translationX <= 250f)
|
||||
|
||||
// The update listener should have been called with a value < 500f, and then a value less
|
||||
// than or equal to the 250f minimum.
|
||||
verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
|
||||
{ u -> u.value < 500f },
|
||||
{ u -> u.value <= 250f })
|
||||
|
||||
// Despite the fact that the fling has ended, the end listener shouldn't have been called
|
||||
// since we're about to begin springing the same property.
|
||||
verifyNoMoreInteractions(mockEndListener)
|
||||
verifyNoMoreInteractions(mockEndAction)
|
||||
|
||||
// Wait for the spring to finish.
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_X)
|
||||
|
||||
// Make sure we continued past 250f since the spring should have been started with some
|
||||
// remaining negative velocity from the fling.
|
||||
verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
|
||||
{ u -> u.value < 250f })
|
||||
|
||||
// At this point, the animation end listener should have been called once, and only once,
|
||||
// when the spring ended at 250f.
|
||||
verify(mockEndListener).onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X,
|
||||
wasFling = false,
|
||||
canceled = false,
|
||||
finalValue = 250f,
|
||||
finalVelocity = 0f,
|
||||
allRelevantPropertyAnimsEnded = true)
|
||||
verifyNoMoreInteractions(mockEndListener)
|
||||
|
||||
// The end action should also have been called once.
|
||||
verify(mockEndAction, times(1)).run()
|
||||
verifyNoMoreInteractions(mockEndAction)
|
||||
|
||||
assertEquals(250f, testView.translationX)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFlingThenSpring_objectOutsideFlingBounds() {
|
||||
// Start the view at x = -500, well outside the fling bounds of min = 0f, with strong
|
||||
// negative velocity.
|
||||
testView.translationX = -500f
|
||||
animator
|
||||
.flingThenSpring(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
-5000f,
|
||||
flingConfig.apply { min = 0f },
|
||||
springConfig)
|
||||
.addUpdateListener(mockUpdateListener)
|
||||
.addEndListener(mockEndListener)
|
||||
.withEndActions(mockEndAction::run)
|
||||
.start()
|
||||
|
||||
// The initial -5000f velocity should result in frames to the left of -500f before the view
|
||||
// springs back towards 0f.
|
||||
verifyAnimationUpdateFrames(
|
||||
animator, DynamicAnimation.TRANSLATION_X,
|
||||
{ u -> u.value < -500f },
|
||||
{ u -> u.value > -500f })
|
||||
|
||||
// We should end up at the fling min.
|
||||
assertEquals(0f, testView.translationX, 1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFlingToMinMaxThenSpring() {
|
||||
// Start at x = 500f.
|
||||
testView.translationX = 500f
|
||||
|
||||
// Fling to the left at the very sad rate of -1 pixels per second. That won't get us much of
|
||||
// anywhere, and certainly not to the 0f min.
|
||||
animator
|
||||
// Good thing we have flingToMinMaxThenSpring!
|
||||
.flingThenSpring(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
-10000f,
|
||||
flingConfig.apply { min = 0f },
|
||||
springConfig,
|
||||
flingMustReachMinOrMax = true)
|
||||
.addUpdateListener(mockUpdateListener)
|
||||
.addEndListener(mockEndListener)
|
||||
.withEndActions(mockEndAction::run)
|
||||
.start()
|
||||
|
||||
// Thanks, flingToMinMaxThenSpring, for adding enough velocity to get us here.
|
||||
assertEquals(0f, testView.translationX, 1f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the calls to the mock update listener match the animation update frames
|
||||
* reported by the test internal listener, in order.
|
||||
|
||||
Reference in New Issue
Block a user