Adds PhysicsAnimator and accompanying test utilities!

This is the first CL, which adds the basic spring/fling and listener capabilities.

Change-Id: I059b82a79bb2f2d93c57998669a6aac7a608cb42
Bug: 138115889
Test: atest SystemUITests
This commit is contained in:
Joshua Tsuji
2019-11-07 18:32:58 -05:00
parent 7084521a8e
commit 408b9595e3
3 changed files with 1610 additions and 0 deletions

View File

@@ -0,0 +1,717 @@
/*
* Copyright (C) 2019 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.util.animation
import android.os.Looper
import android.util.ArrayMap
import android.util.Log
import android.view.View
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FlingAnimation
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.systemui.util.animation.PhysicsAnimator.Companion.getInstance
import java.util.WeakHashMap
/**
* Extension function for all objects which will return a PhysicsAnimator instance for that object.
*/
val <T : View> T.physicsAnimator: PhysicsAnimator<T> get() { return getInstance(this) }
private const val TAG = "PhysicsAnimator"
typealias EndAction = () -> Unit
/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */
typealias UpdateMap<T> =
ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
/**
* Map of the animators associated with a given object. This ensures that only one animator
* per object exists.
*/
internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>()
/**
* Default spring configuration to use for animations where stiffness and/or damping ratio
* were not provided.
*/
private val defaultSpring = PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_MEDIUM,
SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
/** Default fling configuration to use for animations where friction was not provided. */
private val defaultFling = PhysicsAnimator.FlingConfig(
friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE)
/** Whether to log helpful debug information about animations. */
private var verboseLogging = false
/**
* Animator that uses physics-based animations to animate properties on views and objects. Physics
* animations use real-world physical concepts, such as momentum and mass, to realistically simulate
* motion. PhysicsAnimator is heavily inspired by [android.view.ViewPropertyAnimator], and
* also uses the builder pattern to configure and start animations.
*
* The physics animations are backed by [DynamicAnimation].
*
* @param T The type of the object being animated.
*/
class PhysicsAnimator<T> private constructor (val target: T) {
/** Data class for representing animation frame updates. */
data class AnimationUpdate(val value: Float, val velocity: Float)
/** [DynamicAnimation] instances for the given properties. */
private val springAnimations = ArrayMap<FloatPropertyCompat<in T>, SpringAnimation>()
private val flingAnimations = ArrayMap<FloatPropertyCompat<in T>, FlingAnimation>()
/**
* Spring and fling configurations for the properties to be animated on the target. We'll
* configure and start the DynamicAnimations for these properties according to the provided
* configurations.
*/
private val springConfigs = ArrayMap<FloatPropertyCompat<in T>, SpringConfig>()
private val flingConfigs = ArrayMap<FloatPropertyCompat<in T>, FlingConfig>()
/**
* Animation listeners for the animation. These will be notified when each property animation
* updates or ends.
*/
private val updateListeners = ArrayList<UpdateListener<T>>()
private val endListeners = ArrayList<EndListener<T>>()
/** End actions to run when all animations have completed. */
private val endActions = ArrayList<EndAction>()
/**
* Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to
* the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add
* just one permanent update and end listener to the DynamicAnimations.
*/
internal var internalListeners = ArrayList<InternalListener>()
/**
* Action to run when [start] is called. This can be changed by
* [PhysicsAnimatorTestUtils.prepareForTest] to enable animators to run under test and provide
* helpful test utilities.
*/
internal var startAction: () -> Unit = ::startInternal
/**
* Springs a property to the given value, using the provided configuration settings.
*
* Springs are used when you know the exact value to which you want to animate. They can be
* configured with a start velocity (typically used when the spring is initiated by a touch
* event), but this velocity will be realistically attenuated as forces are applied to move the
* property towards the end value.
*
* If you find yourself repeating the same stiffness and damping ratios many times, consider
* storing a single [SpringConfig] instance and passing that in instead of individual values.
*
* @param property The property to spring to the given value. The property must be an instance
* of FloatPropertyCompat&lt;? super T&gt;. For example, if this is a
* PhysicsAnimator&lt;FrameLayout&gt;, you can use a FloatPropertyCompat&lt;FrameLayout&gt;, as
* well as a FloatPropertyCompat&lt;ViewGroup&gt;, and so on.
* @param toPosition The value to spring the given property to.
* @param startVelocity The initial velocity to use for the animation.
* @param stiffness The stiffness to use for the spring. Higher stiffness values result in
* faster animations, while lower stiffness means a slower animation. Reasonable values for
* low, medium, and high stiffness can be found as constants in [SpringForce].
* @param dampingRatio The damping ratio (bounciness) to use for the spring. Higher values
* result in a less 'springy' animation, while lower values allow the animation to bounce
* back and forth for a longer time after reaching the final position. Reasonable values for
* low, medium, and high damping can be found in [SpringForce].
*/
fun spring(
property: FloatPropertyCompat<in T>,
toPosition: Float,
startVelocity: Float = 0f,
stiffness: Float = defaultSpring.stiffness,
dampingRatio: Float = defaultSpring.dampingRatio
): PhysicsAnimator<T> {
if (verboseLogging) {
Log.d(TAG, "Springing ${getReadablePropertyName(property)} to $toPosition.")
}
springConfigs[property] =
SpringConfig(stiffness, dampingRatio, startVelocity, toPosition)
return this
}
/**
* Springs a property to a given value using the provided start velocity and configuration
* options.
*
* @see spring
*/
fun spring(
property: FloatPropertyCompat<in T>,
toPosition: Float,
startVelocity: Float,
config: SpringConfig = defaultSpring
): PhysicsAnimator<T> {
return spring(
property, toPosition, startVelocity, config.stiffness, config.dampingRatio)
}
/**
* Springs a property to a given value using the provided configuration options, and a start
* velocity of 0f.
*
* @see spring
*/
fun spring(
property: FloatPropertyCompat<in T>,
toPosition: Float,
config: SpringConfig = defaultSpring
): PhysicsAnimator<T> {
return spring(property, toPosition, 0f, config)
}
/**
* Flings a property using the given start velocity, using a [FlingAnimation] configured using
* the provided configuration settings.
*
* Flings are used when you have a start velocity, and want the property value to realistically
* decrease as friction is applied until the velocity reaches zero. Flings do not have a
* deterministic end value. If you are attempting to animate to a specific end value, use
* [spring].
*
* If you find yourself repeating the same friction/min/max values, consider storing a single
* [FlingConfig] and passing that in instead.
*
* @param property The property to fling using the given start velocity.
* @param startVelocity The start velocity (in pixels per second) with which to start the fling.
* @param friction Friction value applied to slow down the animation over time. Higher values
* will more quickly slow the animation. Typical friction values range from 1f to 10f.
* @param min The minimum value allowed for the animation. If this value is reached, the
* animation will end abruptly.
* @param max The maximum value allowed for the animation. If this value is reached, the
* animation will end abruptly.
*/
fun fling(
property: FloatPropertyCompat<in T>,
startVelocity: Float,
friction: Float = defaultFling.friction,
min: Float = defaultFling.min,
max: Float = defaultFling.max
): PhysicsAnimator<T> {
if (verboseLogging) {
Log.d(TAG, "Flinging ${getReadablePropertyName(property)} " +
"with velocity $startVelocity.")
}
flingConfigs[property] = FlingConfig(friction, min, max, startVelocity)
return this
}
/**
* Flings a property using the given start velocity, using a [FlingAnimation] configured using
* the provided configuration settings.
*
* @see fling
*/
fun fling(
property: FloatPropertyCompat<in T>,
startVelocity: Float,
config: FlingConfig = defaultFling
): PhysicsAnimator<T> {
return fling(property, startVelocity, config.friction, config.min, config.max)
}
/**
* 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
* and the new property values.
*/
fun addUpdateListener(listener: UpdateListener<T>): PhysicsAnimator<T> {
updateListeners.add(listener)
return this
}
/**
* Adds a listener that will be called whenever a property's animation ends. 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].
*/
fun addEndListener(listener: EndListener<T>): PhysicsAnimator<T> {
endListeners.add(listener)
return this
}
/**
* Adds end actions that will be run sequentially when animations for every property involved in
* this specific animation have ended (unless they were explicitly canceled). For example, if
* you call:
*
* animator
* .spring(TRANSLATION_X, ...)
* .spring(TRANSLATION_Y, ...)
* .withEndAction(action)
* .start()
*
* 'action' will be run when both TRANSLATION_X and TRANSLATION_Y end.
*
* Other properties may still be animating, if those animations were not started in the same
* call. For example:
*
* animator
* .spring(ALPHA, ...)
* .start()
*
* animator
* .spring(TRANSLATION_X, ...)
* .spring(TRANSLATION_Y, ...)
* .withEndAction(action)
* .start()
*
* 'action' will still be run as soon as TRANSLATION_X and TRANSLATION_Y end, even if ALPHA is
* still animating.
*
* If you want to run actions as soon as a subset of property animations have ended, you want
* access to the animation's end value/velocity, or you want to run these actions even if the
* animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param,
* which indicates that all relevant animations have ended.
*/
fun withEndActions(vararg endActions: EndAction): PhysicsAnimator<T> {
this.endActions.addAll(endActions)
return this
}
/** Starts the animations! */
fun start() {
startAction()
}
/**
* Starts the animations for real! This is typically called immediately by [start] unless this
* animator is under test.
*/
internal fun startInternal() {
if (!Looper.getMainLooper().isCurrentThread) {
Log.e(TAG, "Animations can only be started on the main thread. If you are seeing " +
"this message in a test, call PhysicsAnimatorTestUtils#prepareForTest in " +
"your test setup.")
}
// Add an internal listener that will dispatch animation events to the provided listeners.
internalListeners.add(InternalListener(
getAnimatedProperties(),
ArrayList(updateListeners),
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()
}
clearAnimator()
}
/** Clear the animator's builder variables. */
private fun clearAnimator() {
springConfigs.clear()
flingConfigs.clear()
updateListeners.clear()
endListeners.clear()
endActions.clear()
}
/** Retrieves a spring animation for the given property, building one if needed. */
private fun getSpringAnimation(property: FloatPropertyCompat<in T>): SpringAnimation {
return springAnimations.getOrPut(
property,
{ configureDynamicAnimation(SpringAnimation(target, property), property)
as SpringAnimation })
}
/** Retrieves a fling animation for the given property, building one if needed. */
private fun getFlingAnimation(property: FloatPropertyCompat<in T>): FlingAnimation {
return flingAnimations.getOrPut(
property,
{ configureDynamicAnimation(FlingAnimation(target, property), property)
as FlingAnimation })
}
/**
* Adds update and end listeners to the DynamicAnimation which will dispatch to the internal
* listeners.
*/
private fun configureDynamicAnimation(
anim: DynamicAnimation<*>,
property: FloatPropertyCompat<in T>
): DynamicAnimation<*> {
anim.addUpdateListener { _, value, velocity ->
for (i in 0 until internalListeners.size) {
internalListeners[i].onInternalAnimationUpdate(property, value, velocity)
}
}
anim.addEndListener { _, canceled, value, velocity ->
internalListeners.removeAll {
it.onInternalAnimationEnd(property, canceled, value, velocity) } }
return anim
}
/**
* Internal listener class that receives updates from DynamicAnimation listeners, and dispatches
* them to the appropriate update/end listeners. This class is also aware of which properties
* were being animated when the end listeners were passed in, so that we can provide the
* appropriate value for allEnded to [EndListener.onAnimationEnd].
*/
internal inner class InternalListener constructor(
private var properties: Set<FloatPropertyCompat<in T>>,
private var updateListeners: List<UpdateListener<T>>,
private var endListeners: List<EndListener<T>>,
private var endActions: List<EndAction>
) {
/** The number of properties whose animations haven't ended. */
private var numPropertiesAnimating = properties.size
/**
* Update values that haven't yet been dispatched because not all property animations have
* updated yet.
*/
private val undispatchedUpdates =
ArrayMap<FloatPropertyCompat<in T>, AnimationUpdate>()
/** Called when a DynamicAnimation updates. */
internal fun onInternalAnimationUpdate(
property: FloatPropertyCompat<in T>,
value: Float,
velocity: Float
) {
// If this property animation isn't relevant to this listener, ignore it.
if (!properties.contains(property)) {
return
}
undispatchedUpdates[property] = AnimationUpdate(value, velocity)
maybeDispatchUpdates()
}
/**
* Called when a DynamicAnimation ends.
*
* @return True if this listener should be removed from the list of internal listeners, so
* it no longer receives updates from DynamicAnimations.
*/
internal fun onInternalAnimationEnd(
property: FloatPropertyCompat<in T>,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float
): Boolean {
// If this property animation isn't relevant to this listener, ignore it.
if (!properties.contains(property)) {
return false
}
// Dispatch updates if we have one for each property.
numPropertiesAnimating--
maybeDispatchUpdates()
// If we didn't have an update for each property, dispatch the update for the ending
// property. This guarantees that an update isn't sent for this property *after* we call
// onAnimationEnd for that property.
if (undispatchedUpdates.contains(property)) {
updateListeners.forEach { updateListener ->
updateListener.onAnimationUpdateForProperty(
target,
UpdateMap<T>().also { it[property] = undispatchedUpdates[property] })
}
undispatchedUpdates.remove(property)
}
val allEnded = !arePropertiesAnimating(properties)
endListeners.forEach {
it.onAnimationEnd(target, property, canceled, finalValue, finalVelocity, allEnded) }
// If all of the animations that this listener cares about have ended, run the end
// actions unless the animation was canceled.
if (allEnded && !canceled) {
endActions.forEach { it() }
}
return allEnded
}
/**
* Dispatch undispatched values if we've received an update from each of the animating
* properties.
*/
private fun maybeDispatchUpdates() {
if (undispatchedUpdates.size >= numPropertiesAnimating &&
undispatchedUpdates.size > 0) {
updateListeners.forEach {
it.onAnimationUpdateForProperty(target, ArrayMap(undispatchedUpdates))
}
undispatchedUpdates.clear()
}
}
}
/** Return true if any animations are running on the object. */
fun isRunning(): Boolean {
return arePropertiesAnimating(springAnimations.keys.union(flingAnimations.keys))
}
/** Returns whether the given property is animating. */
fun isPropertyAnimating(property: FloatPropertyCompat<in T>): Boolean {
return springAnimations[property]?.isRunning ?: false
}
/** Returns whether any of the given properties are animating. */
fun arePropertiesAnimating(properties: Set<FloatPropertyCompat<in T>>): Boolean {
return properties.any { isPropertyAnimating(it) }
}
/** Return the set of properties that will begin animating upon calling [start]. */
internal fun getAnimatedProperties(): Set<FloatPropertyCompat<in T>> {
return springConfigs.keys.union(flingConfigs.keys)
}
/** Cancels all in progress animations on all properties. */
fun cancel() {
for (dynamicAnim in flingAnimations.values.union(springAnimations.values)) {
dynamicAnim.cancel()
}
}
/**
* Container object for spring animation configuration settings. This allows you to store
* default stiffness and damping ratio values in a single configuration object, which you can
* pass to [spring].
*/
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
) {
constructor() :
this(defaultSpring.stiffness, defaultSpring.dampingRatio)
constructor(stiffness: Float, dampingRatio: Float) :
this(stiffness = stiffness, dampingRatio = dampingRatio, startVel = 0f)
/** Apply these configuration settings to the given SpringAnimation. */
internal fun applyToAnimation(anim: SpringAnimation) {
val springForce = anim.spring ?: SpringForce()
anim.spring = springForce.apply {
stiffness = this@SpringConfig.stiffness
dampingRatio = this@SpringConfig.dampingRatio
finalPosition = this@SpringConfig.finalPosition
}
if (startVel != 0f) anim.setStartVelocity(startVel)
}
}
/**
* Container object for fling animation configuration settings. This allows you to store default
* friction values (as well as optional min/max values) in a single configuration object, which
* you can pass to [fling] and related methods.
*/
data class FlingConfig internal constructor(
internal var friction: Float,
internal var min: Float,
internal var max: Float,
internal var startVel: Float
) {
constructor() : this(defaultFling.friction)
constructor(friction: Float) :
this(friction, defaultFling.min, defaultFling.max)
constructor(friction: Float, min: Float, max: Float) :
this(friction, min, max, startVel = 0f)
/** Apply these configuration settings to the given FlingAnimation. */
internal fun applyToAnimation(anim: FlingAnimation) {
anim.apply {
friction = this@FlingConfig.friction
setMinValue(min)
setMaxValue(max)
setStartVelocity(startVel)
}
}
}
/**
* Listener for receiving values from in progress animations. Used with
* [PhysicsAnimator.addUpdateListener].
*
* @param <T> The type of the object being animated.
</T> */
interface UpdateListener<T> {
/**
* Called on each animation frame with the target object, and a map of FloatPropertyCompat
* -> AnimationUpdate, containing the latest value and velocity for that property. When
* multiple properties are animating together, the map will typically contain one entry for
* each property. However, you should never assume that this is the case - when a property
* animation ends earlier than the others, you'll receive an UpdateMap containing only that
* property's final update. Subsequently, you'll only receive updates for the properties
* that are still animating.
*
* Always check that the map contains an update for the property you're interested in before
* accessing it.
*
* @param target The animated object itself.
* @param values Map of property to AnimationUpdate, which contains that property
* animation's latest value and velocity. You should never assume that a particular property
* is present in this map.
*/
fun onAnimationUpdateForProperty(
target: T,
values: UpdateMap<T>
)
}
/**
* Listener for receiving callbacks when animations end.
*
* @param <T> The type of the object being animated.
</T> */
interface EndListener<T> {
/**
* Called with the final animation values as each property animation ends. This can be used
* to respond to specific property animations concluding (such as hiding a view when ALPHA
* ends, even if the corresponding TRANSLATION animations have not ended).
*
* If you just want to run an action when all of the property animations have ended, you can
* use [PhysicsAnimator.withEndActions].
*
* @param target The animated object itself.
* @param property The property whose animation has just ended.
* @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.
* This is typically zero, unless this was a fling animation which ended abruptly due to
* reaching its configured min/max values.
* @param allRelevantPropertyAnimsEnded Whether all properties relevant to this end listener
* have ended. Relevant properties are those which were animated alongside the
* [addEndListener] call where this animator was passed in. For example:
*
* animator
* .spring(TRANSLATION_X, 100f)
* .spring(TRANSLATION_Y, 200f)
* .withEndListener(firstEndListener)
* .start()
*
* firstEndListener will be called first for TRANSLATION_X, with allEnded = false,
* because TRANSLATION_Y is still running. When TRANSLATION_Y ends, it'll be called with
* allEnded = true.
*
* If a subsequent call to start() is made with other properties, those properties are not
* considered relevant and allEnded will still equal true when only TRANSLATION_X and
* TRANSLATION_Y end. For example, if immediately after the prior example, while
* TRANSLATION_X and TRANSLATION_Y are still animating, we called:
*
* animator.
* .spring(SCALE_X, 2f, stiffness = 10f) // That will take awhile...
* .withEndListener(secondEndListener)
* .start()
*
* firstEndListener will still be called with allEnded = true when TRANSLATION_X/Y end, even
* though SCALE_X is still animating. Similarly, secondEndListener will be called with
* allEnded = true as soon as SCALE_X ends, even if the translation animations are still
* running.
*/
fun onAnimationEnd(
target: T,
property: FloatPropertyCompat<in T>,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float,
allRelevantPropertyAnimsEnded: Boolean
)
}
companion object {
/**
* Constructor to use to for new physics animator instances in [getInstance]. This is
* typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that
* all code using the physics animator is given testable instances instead.
*/
internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator
@JvmStatic
fun <T : Any> getInstance(target: T): PhysicsAnimator<T> {
if (!animators.containsKey(target)) {
animators[target] = instanceConstructor(target)
}
return animators[target] as PhysicsAnimator<T>
}
/**
* Set whether all physics animators should log a lot of information about animations.
* Useful for debugging!
*/
@JvmStatic
fun setVerboseLogging(debug: Boolean) {
verboseLogging = debug
}
@JvmStatic
fun getReadablePropertyName(property: FloatPropertyCompat<*>): String {
return when (property) {
DynamicAnimation.TRANSLATION_X -> "translationX"
DynamicAnimation.TRANSLATION_Y -> "translationY"
DynamicAnimation.TRANSLATION_Z -> "translationZ"
DynamicAnimation.SCALE_X -> "scaleX"
DynamicAnimation.SCALE_Y -> "scaleY"
DynamicAnimation.ROTATION -> "rotation"
DynamicAnimation.ROTATION_X -> "rotationX"
DynamicAnimation.ROTATION_Y -> "rotationY"
DynamicAnimation.SCROLL_X -> "scrollX"
DynamicAnimation.SCROLL_Y -> "scrollY"
DynamicAnimation.ALPHA -> "alpha"
else -> "Custom FloatPropertyCompat instance"
}
}
}
}

View File

@@ -0,0 +1,457 @@
/*
* Copyright (C) 2019 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.util.animation
import android.os.Handler
import android.os.Looper
import android.util.ArrayMap
import androidx.dynamicanimation.animation.FloatPropertyCompat
import java.util.ArrayDeque
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
typealias UpdateMatcher = (PhysicsAnimator.AnimationUpdate) -> Boolean
typealias UpdateFramesPerProperty<T> =
ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>
/**
* Utilities for testing code that uses [PhysicsAnimator].
*
* Start by calling [prepareForTest] at the beginning of each test - this will modify the behavior
* of all PhysicsAnimator instances so that they post animations to the main thread (so they don't
* crash). It'll also enable the use of the other static helper methods in this class, which you can
* use to do things like block the test until animations complete (so you can test end states), or
* verify keyframes.
*/
object PhysicsAnimatorTestUtils {
var timeoutMs: Long = 2000
private var startBlocksUntilAnimationsEnd = false
private val animationThreadHandler = Handler(Looper.getMainLooper())
private val allAnimatedObjects = HashSet<Any>()
private val animatorTestHelpers = HashMap<PhysicsAnimator<*>, AnimatorTestHelper<*>>()
/**
* Modifies the behavior of all [PhysicsAnimator] instances so that they post animations to the
* main thread, and report all of their
*/
@JvmStatic
fun prepareForTest() {
val defaultConstructor = PhysicsAnimator.instanceConstructor
PhysicsAnimator.instanceConstructor = fun(target: Any): PhysicsAnimator<*> {
val animator = defaultConstructor(target)
allAnimatedObjects.add(target)
animatorTestHelpers[animator] = AnimatorTestHelper(animator)
return animator
}
timeoutMs = 2000
startBlocksUntilAnimationsEnd = false
allAnimatedObjects.clear()
}
@JvmStatic
fun tearDown() {
val latch = CountDownLatch(1)
animationThreadHandler.post {
animatorTestHelpers.keys.forEach { it.cancel() }
latch.countDown()
}
latch.await()
animatorTestHelpers.clear()
animators.clear()
allAnimatedObjects.clear()
}
/**
* Sets the maximum time (in milliseconds) to block the test thread while waiting for animations
* before throwing an exception.
*/
@JvmStatic
fun setBlockTimeout(timeoutMs: Long) {
this.timeoutMs = timeoutMs
}
/**
* Sets whether all animations should block the test thread until they end. This is typically
* the desired behavior, since you can invoke code that runs an animation and then assert things
* about its end state.
*/
@JvmStatic
fun setAllAnimationsBlock(block: Boolean) {
startBlocksUntilAnimationsEnd = block
}
/**
* Blocks the calling thread until animations of the given property on the target object end.
*/
@JvmStatic
@Throws(InterruptedException::class)
fun <T : Any> blockUntilAnimationsEnd(
animator: PhysicsAnimator<T>,
vararg properties: FloatPropertyCompat<in T>
) {
val animatingProperties = HashSet<FloatPropertyCompat<in T>>()
for (property in properties) {
if (animator.isPropertyAnimating(property)) {
animatingProperties.add(property)
}
}
if (animatingProperties.size > 0) {
val latch = CountDownLatch(animatingProperties.size)
getAnimationTestHelper(animator).addTestEndListener(
object : PhysicsAnimator.EndListener<T> {
override fun onAnimationEnd(
target: T,
property: FloatPropertyCompat<in T>,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float,
allRelevantPropertyAnimsEnded: Boolean
) {
if (animatingProperties.contains(property)) {
latch.countDown()
}
}
})
latch.await(timeoutMs, TimeUnit.MILLISECONDS)
}
}
/**
* Blocks the calling thread until all animations of the given property (on all target objects)
* have ended. Useful when you don't have access to the objects being animated, but still need
* to wait for them to end so that other testable side effects occur (such as update/end
* listeners).
*/
@JvmStatic
@Throws(InterruptedException::class)
fun <T : Any> blockUntilAnimationsEnd(
properties: FloatPropertyCompat<in T>
) {
for (target in allAnimatedObjects) {
try {
blockUntilAnimationsEnd(
PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, properties)
} catch (e: ClassCastException) {
// Keep checking the other objects for ones whose types match the provided
// properties.
}
}
}
/**
* Blocks the calling thread until the first animation frame in which predicate returns true. If
* the given object isn't animating, returns without blocking.
*/
@JvmStatic
@Throws(InterruptedException::class)
fun <T : Any> blockUntilFirstAnimationFrameWhereTrue(
animator: PhysicsAnimator<T>,
predicate: (T) -> Boolean
) {
if (animator.isRunning()) {
val latch = CountDownLatch(1)
getAnimationTestHelper(animator).addTestUpdateListener(object : PhysicsAnimator
.UpdateListener<T> {
override fun onAnimationUpdateForProperty(
target: T,
values: UpdateMap<T>
) {
if (predicate(target)) {
latch.countDown()
}
}
})
latch.await(timeoutMs, TimeUnit.MILLISECONDS)
}
}
/**
* Verifies that the animator reported animation frame values to update listeners that satisfy
* the given matchers, in order. Not all frames need to satisfy a matcher - we'll run through
* all animation frames, and check them against the current predicate. If it returns false, we
* continue through the frames until it returns true, and then move on to the next matcher.
* Verification fails if we run out of frames while unsatisfied matchers remain.
*
* If verification is successful, all frames to this point are considered 'verified' and will be
* cleared. Subsequent calls to this method will start verification at the next animation frame.
*
* Example: Verify that an animation surpassed x = 50f before going negative.
* verifyAnimationUpdateFrames(
* animator, TRANSLATION_X,
* { u -> u.value > 50f },
* { u -> u.value < 0f })
*
* Example: verify that an animation went backwards at some point while still being on-screen.
* verifyAnimationUpdateFrames(
* animator, TRANSLATION_X,
* { u -> u.velocity < 0f && u.value >= 0f })
*
* This method is intended to help you test longer, more complicated animations where it's
* critical that certain values were reached. Using this method to test short animations can
* fail due to the animation having fewer frames than provided matchers. For example, an
* animation from x = 1f to x = 5f might only have two frames, at x = 3f and x = 5f. The
* following would then fail despite it seeming logically sound:
*
* verifyAnimationUpdateFrames(
* animator, TRANSLATION_X,
* { u -> u.value > 1f },
* { u -> u.value > 2f },
* { u -> u.value > 3f })
*
* Tests might also fail if your matchers are too granular, such as this example test after an
* animation from x = 0f to x = 100f. It's unlikely there was a frame specifically between 2f
* and 3f.
*
* verifyAnimationUpdateFrames(
* animator, TRANSLATION_X,
* { u -> u.value > 2f && u.value < 3f },
* { u -> u.value >= 50f })
*
* Failures will print a helpful log of all animation frames so you can see what caused the test
* to fail.
*/
fun <T : Any> verifyAnimationUpdateFrames(
animator: PhysicsAnimator<T>,
property: FloatPropertyCompat<in T>,
firstUpdateMatcher: UpdateMatcher,
vararg additionalUpdateMatchers: UpdateMatcher
) {
val updateFrames: UpdateFramesPerProperty<T> = getAnimationUpdateFrames(animator)
val matchers = ArrayDeque<UpdateMatcher>(
additionalUpdateMatchers.toList())
val frameTraceMessage = StringBuilder()
var curMatcher = firstUpdateMatcher
// Loop through the updates from the testable animator.
for (update in updateFrames[property]
?: error("No frames for given target object and property.")) {
// Check whether this frame satisfies the current matcher.
if (curMatcher(update)) {
// If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining
// frames and return without failing.
if (matchers.size == 0) {
getAnimationUpdateFrames(animator).remove(property)
return
}
frameTraceMessage.append("$update\t(satisfied matcher)\n")
curMatcher = matchers.pop() // Get the next matcher and keep going.
} else {
frameTraceMessage.append("${update}\n")
}
}
val readablePropertyName = PhysicsAnimator.getReadablePropertyName(property)
getAnimationUpdateFrames(animator).remove(property)
throw RuntimeException(
"Failed to verify animation frames for property $readablePropertyName: " +
"Provided ${additionalUpdateMatchers.size + 1} matchers, " +
"however ${matchers.size + 1} remained unsatisfied.\n\n" +
"All frames:\n$frameTraceMessage")
}
/**
* Overload of [verifyAnimationUpdateFrames] that builds matchers for you, from given float
* values. For example, to verify that an animations passed from 0f to 50f to 100f back to 50f:
*
* verifyAnimationUpdateFrames(animator, TRANSLATION_X, 0f, 50f, 100f, 50f)
*
* This verifies that update frames were received with values of >= 0f, >= 50f, >= 100f, and
* <= 50f.
*
* The same caveats apply: short animations might not have enough frames to satisfy all of the
* matchers, and overly specific calls (such as 0f, 1f, 2f, 3f, etc. for an animation from
* x = 0f to x = 100f) might fail as the animation only had frames at 0f, 25f, 50f, 75f, and
* 100f. As with [verifyAnimationUpdateFrames], failures will print a helpful log of all frames
* so you can see what caused the test to fail.
*/
fun <T : Any> verifyAnimationUpdateFrames(
animator: PhysicsAnimator<T>,
property: FloatPropertyCompat<in T>,
startValue: Float,
firstTargetValue: Float,
vararg additionalTargetValues: Float
) {
val matchers = ArrayList<UpdateMatcher>()
val values = ArrayList<Float>().also {
it.add(firstTargetValue)
it.addAll(additionalTargetValues.toList())
}
var prevVal = startValue
for (value in values) {
if (value > prevVal) {
matchers.add { update -> update.value >= value }
} else {
matchers.add { update -> update.value <= value }
}
prevVal = value
}
verifyAnimationUpdateFrames(
animator, property, matchers[0], *matchers.drop(0).toTypedArray())
}
/**
* Returns all of the values that have ever been reported to update listeners, per property.
*/
fun <T : Any> getAnimationUpdateFrames(animator: PhysicsAnimator<T>):
UpdateFramesPerProperty<T> {
return animatorTestHelpers[animator]?.getUpdates() as UpdateFramesPerProperty<T>
}
/**
* Clears animation frame updates from the given animator so they aren't used the next time its
* passed to [verifyAnimationUpdateFrames].
*/
fun <T : Any> clearAnimationUpdateFrames(animator: PhysicsAnimator<T>) {
animatorTestHelpers[animator]?.clearUpdates()
}
private fun <T> getAnimationTestHelper(animator: PhysicsAnimator<T>): AnimatorTestHelper<T> {
return animatorTestHelpers[animator] as AnimatorTestHelper<T>
}
/**
* Helper class for testing an animator. This replaces the animator's start action with
* [startForTest] and adds test listeners to enable other test utility behaviors. We build one
* these for each Animator and keep them around so we can access the updates.
*/
class AnimatorTestHelper<T> (private val animator: PhysicsAnimator<T>) {
/** All updates received for each property animation. */
private val allUpdates =
ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>()
private val testEndListeners = ArrayList<PhysicsAnimator.EndListener<T>>()
private val testUpdateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>()
init {
animator.startAction = ::startForTest
}
internal fun addTestEndListener(listener: PhysicsAnimator.EndListener<T>) {
testEndListeners.add(listener)
}
internal fun addTestUpdateListener(listener: PhysicsAnimator.UpdateListener<T>) {
testUpdateListeners.add(listener)
}
internal fun getUpdates(): UpdateFramesPerProperty<T> {
return allUpdates
}
internal fun clearUpdates() {
allUpdates.clear()
}
private fun startForTest() {
// The testable animator needs to block the main thread until super.start() has been
// called, since callers expect .start() to be synchronous but we're posting it to a
// handler here. We may also continue blocking until all animations end, if
// startBlocksUntilAnimationsEnd = true.
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> {
override fun onAnimationUpdateForProperty(
target: T,
values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
) {
for (listener in testUpdateListeners) {
listener.onAnimationUpdateForProperty(target, values)
}
}
})
// Add an end listener that dispatches to any test end listeners added by tests, and
// unblocks the main thread if required.
animator.addEndListener(object : PhysicsAnimator.EndListener<T> {
override fun onAnimationEnd(
target: T,
property: FloatPropertyCompat<in T>,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float,
allRelevantPropertyAnimsEnded: Boolean
) {
for (listener in testEndListeners) {
listener.onAnimationEnd(
target, property, canceled, finalValue, finalVelocity,
allRelevantPropertyAnimsEnded)
}
if (allRelevantPropertyAnimsEnded) {
testEndListeners.clear()
testUpdateListeners.clear()
if (startBlocksUntilAnimationsEnd) {
unblockLatch.countDown()
}
}
}
})
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()
}
unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS)
}
}
}

View File

@@ -0,0 +1,436 @@
package com.android.systemui.util.animation
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.util.ArrayMap
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringForce
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.animation.PhysicsAnimator.EndListener
import com.android.systemui.util.animation.PhysicsAnimator.UpdateListener
import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames
import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames
import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames
import org.junit.After
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
@TestableLooper.RunWithLooper
@RunWith(AndroidTestingRunner::class)
@SmallTest
class PhysicsAnimatorTest : SysuiTestCase() {
private lateinit var viewGroup: ViewGroup
private lateinit var testView: View
private lateinit var testView2: View
private lateinit var animator: PhysicsAnimator<View>
private val springConfig = PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY)
private val flingConfig = PhysicsAnimator.FlingConfig(2f)
private lateinit var mockUpdateListener: UpdateListener<View>
private lateinit var mockEndListener: EndListener<View>
private lateinit var mockEndAction: Runnable
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
mockUpdateListener = mock(UpdateListener::class.java) as UpdateListener<View>
mockEndListener = mock(EndListener::class.java) as EndListener<View>
mockEndAction = mock(Runnable::class.java)
viewGroup = FrameLayout(context)
testView = View(context)
testView2 = View(context)
viewGroup.addView(testView)
viewGroup.addView(testView2)
PhysicsAnimatorTestUtils.prepareForTest()
// Most of our tests involve checking the end state of animations, so we want calls that
// start animations to block the test thread until the animations have ended.
PhysicsAnimatorTestUtils.setAllAnimationsBlock(true)
animator = PhysicsAnimator.getInstance(testView)
}
@After
fun tearDown() {
PhysicsAnimatorTestUtils.tearDown()
}
@Test
fun testOneAnimatorPerView() {
assertEquals(animator, PhysicsAnimator.getInstance(testView))
assertEquals(PhysicsAnimator.getInstance(testView), PhysicsAnimator.getInstance(testView))
assertNotEquals(animator, PhysicsAnimator.getInstance(testView2))
}
@Test
fun testSpringOneProperty() {
animator
.spring(DynamicAnimation.TRANSLATION_X, 50f, springConfig)
.start()
assertEquals(testView.translationX, 50f, 1f)
}
@Test
fun testSpringMultipleProperties() {
animator
.spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig)
.spring(DynamicAnimation.TRANSLATION_Y, 50f, springConfig)
.spring(DynamicAnimation.SCALE_Y, 1.1f, springConfig)
.start()
assertEquals(10f, testView.translationX, 1f)
assertEquals(50f, testView.translationY, 1f)
assertEquals(1.1f, testView.scaleY, 0.01f)
}
@Test
fun testFling() {
val startTime = System.currentTimeMillis()
animator
.fling(DynamicAnimation.TRANSLATION_X, 1000f /* startVelocity */, flingConfig)
.fling(DynamicAnimation.TRANSLATION_Y, 500f, flingConfig)
.start()
val elapsedTimeSeconds = (System.currentTimeMillis() - startTime) / 1000f
// If the fling worked, the view should be somewhere between its starting position and the
// and the theoretical no-friction maximum of startVelocity (in pixels per second)
// multiplied by elapsedTimeSeconds. We can't calculate an exact expected location for a
// fling, so this is close enough.
assertTrue(testView.translationX > 0f)
assertTrue(testView.translationX < 1000f * elapsedTimeSeconds)
assertTrue(testView.translationY > 0f)
assertTrue(testView.translationY < 500f * elapsedTimeSeconds)
}
@Test
@Throws(InterruptedException::class)
fun testEndListenersAndActions() {
PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
animator
.spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig)
.spring(DynamicAnimation.TRANSLATION_Y, 500f, springConfig)
.addEndListener(mockEndListener)
.withEndActions(mockEndAction::run)
.start()
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X)
// Once TRANSLATION_X is done, the view should be at x = 10...
assertEquals(10f, testView.translationX, 1f)
// / ...TRANSLATION_Y should still be running...
assertTrue(animator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y))
// ...and our end listener should have been called with x = 10, velocity = 0, and allEnded =
// false since TRANSLATION_Y is still running.
verify(mockEndListener).onAnimationEnd(
testView,
DynamicAnimation.TRANSLATION_X,
canceled = false,
finalValue = 10f,
finalVelocity = 0f,
allRelevantPropertyAnimsEnded = false)
verifyNoMoreInteractions(mockEndListener)
// The end action should not have been run yet.
verify(mockEndAction, times(0)).run()
// Block until TRANSLATION_Y finishes.
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y)
// The view should have been moved.
assertEquals(10f, testView.translationX, 1f)
assertEquals(500f, testView.translationY, 1f)
// The end listener should have been called, this time with TRANSLATION_Y, y = 50, and
// allEnded = true.
verify(mockEndListener).onAnimationEnd(
testView,
DynamicAnimation.TRANSLATION_Y,
canceled = false,
finalValue = 500f,
finalVelocity = 0f,
allRelevantPropertyAnimsEnded = true)
verifyNoMoreInteractions(mockEndListener)
// Now that all properties are done animating, the end action should have been called.
verify(mockEndAction, times(1)).run()
}
@Test
fun testUpdateListeners() {
animator
.spring(DynamicAnimation.TRANSLATION_X, 100f, springConfig)
.spring(DynamicAnimation.TRANSLATION_Y, 50f, springConfig)
.addUpdateListener(object : UpdateListener<View> {
override fun onAnimationUpdateForProperty(
target: View,
values: UpdateMap<View>
) {
mockUpdateListener.onAnimationUpdateForProperty(target, values)
}
})
.start()
verifyUpdateListenerCalls(animator, mockUpdateListener)
}
@Test
fun testListenersNotCalledOnSubsequentAnimations() {
animator
.spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig)
.addUpdateListener(mockUpdateListener)
.addEndListener(mockEndListener)
.withEndActions(mockEndAction::run)
.start()
verifyUpdateListenerCalls(animator, mockUpdateListener)
verify(mockEndListener, times(1)).onAnimationEnd(
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), anyFloat(), anyFloat(),
eq(true))
verify(mockEndAction, times(1)).run()
animator
.spring(DynamicAnimation.TRANSLATION_X, 0f, springConfig)
.start()
// We didn't pass any of the listeners/actions to the subsequent animation, so they should
// never have been called.
verifyNoMoreInteractions(mockUpdateListener)
verifyNoMoreInteractions(mockEndListener)
verifyNoMoreInteractions(mockEndAction)
}
@Test
@Throws(InterruptedException::class)
fun testAnimationsUpdatedWhileInMotion() {
PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
// Spring towards x = 100f.
animator
.spring(
DynamicAnimation.TRANSLATION_X,
100f,
springConfig)
.start()
// Block until it reaches x = 50f.
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(
animator) { view -> view.translationX > 50f }
// Translation X value at the time of reversing the animation to spring to x = 0f.
val reversalTranslationX = testView.translationX
// Spring back towards 0f.
animator
.spring(
DynamicAnimation.TRANSLATION_X,
0f,
// Lower the stiffness to ensure the update listener receives at least one
// update frame where the view has continued to move to the right.
springConfig.apply { stiffness = SpringForce.STIFFNESS_LOW })
.start()
// Wait for TRANSLATION_X.
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X)
// Verify that the animation continued past the X value at the time of reversal, before
// springing back. This ensures the change in direction was not abrupt.
verifyAnimationUpdateFrames(
animator, DynamicAnimation.TRANSLATION_X,
{ u -> u.value > reversalTranslationX },
{ u -> u.value < reversalTranslationX })
// Verify that the view is where it should be.
assertEquals(0f, testView.translationX, 1f)
}
@Test
@Throws(InterruptedException::class)
fun testAnimationsUpdatedWhileInMotion_originalListenersStillCalled() {
PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
// Spring TRANSLATION_X to 100f, with an update and end listener provided.
animator
.spring(DynamicAnimation.TRANSLATION_X, 100f, springConfig)
.addUpdateListener(mockUpdateListener)
.addEndListener(mockEndListener)
.start()
// Wait until the animation is halfway there.
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(
animator) { view -> view.translationX > 50f }
// The end listener shouldn't have been called since the animation hasn't ended.
verifyNoMoreInteractions(mockEndListener)
// Make sure we called the update listener with appropriate values.
verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
{ u -> u.value > 0f },
{ u -> u.value >= 50f })
// Mock a second end listener.
val secondEndListener = mock(EndListener::class.java) as EndListener<View>
val secondUpdateListener = mock(UpdateListener::class.java) as UpdateListener<View>
// Start a new animation that springs both TRANSLATION_X and TRANSLATION_Y, and provide it
// the second end listener. This new end listener should be called for the end of
// TRANSLATION_X and TRANSLATION_Y, with allEnded = true when both have ended.
animator
.spring(DynamicAnimation.TRANSLATION_X, 200f, springConfig)
.spring(DynamicAnimation.TRANSLATION_Y, 4000f, springConfig)
.addUpdateListener(secondUpdateListener)
.addEndListener(secondEndListener)
.start()
// Wait for TRANSLATION_X to end.
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X)
// The update listener provided to the initial animation call (the one that only animated
// TRANSLATION_X) should have been called with values on the way to x = 200f. This is
// because the second animation call updated the original TRANSLATION_X animation.
verifyAnimationUpdateFrames(
animator, DynamicAnimation.TRANSLATION_X,
{ u -> u.value > 100f }, { u -> u.value >= 200f })
// 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)
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)
verifyNoMoreInteractions(secondEndListener)
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y)
// The original end listener shouldn't receive any callbacks because it was not provided to
// an animator that animated TRANSLATION_Y.
verifyNoMoreInteractions(mockEndListener)
verify(secondEndListener, times(1))
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y, false, 4000f, 0f, true)
verifyNoMoreInteractions(secondEndListener)
}
@Test
fun testFlingRespectsMinMax() {
animator
.fling(DynamicAnimation.TRANSLATION_X,
startVelocity = 1000f,
friction = 1.1f,
max = 10f)
.addEndListener(mockEndListener)
.start()
// Ensure that the view stopped at x = 10f, and the end listener was called once with that
// value.
assertEquals(10f, testView.translationX, 1f)
verify(mockEndListener, times(1))
.onAnimationEnd(
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(10f),
anyFloat(), eq(true))
animator
.fling(
DynamicAnimation.TRANSLATION_X,
startVelocity = -1000f,
friction = 1.1f,
min = -5f)
.addEndListener(mockEndListener)
.start()
// Ensure that the view stopped at x = -5f, and the end listener was called once with that
// value.
assertEquals(-5f, testView.translationX, 1f)
verify(mockEndListener, times(1))
.onAnimationEnd(
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(-5f),
anyFloat(), eq(true))
}
@Test
fun testExtensionProperty() {
testView
.physicsAnimator
.spring(DynamicAnimation.TRANSLATION_X, 200f)
.start()
assertEquals(200f, 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.
*/
private fun <T : Any> verifyUpdateListenerCalls(
animator: PhysicsAnimator<T>,
mockUpdateListener: UpdateListener<T>
) {
val updates = getAnimationUpdateFrames(animator)
for (invocation in Mockito.mockingDetails(mockUpdateListener).invocations) {
// Grab the update map of Property -> AnimationUpdate that was passed to the mock update
// listener.
val updateMap = invocation.arguments[1]
as ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
//
for ((property, update) in updateMap) {
val updatesForProperty = updates[property]!!
// This update should be the next one in the list for this property.
if (update != updatesForProperty[0]) {
Assert.fail("The update listener was called with an unexpected value: $update.")
}
updatesForProperty.remove(update)
}
// Mark this invocation verified.
verify(mockUpdateListener).onAnimationUpdateForProperty(animator.target, updateMap)
}
verifyNoMoreInteractions(mockUpdateListener)
// Since we were removing values as matching invocations were found, there should no longer
// be any values remaining. If there are, it means the update listener wasn't notified when
// it should have been.
assertEquals(0,
updates.values.fold(0, { count, propertyUpdates -> count + propertyUpdates.size }))
clearAnimationUpdateFrames(animator)
}
}