Merge "Adds PhysicsAnimator and accompanying test utilities!"
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt
new file mode 100644
index 0000000..62ae7b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt
@@ -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"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt
new file mode 100644
index 0000000..a1f74eb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt
@@ -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)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt
new file mode 100644
index 0000000..a39fbc4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt
@@ -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)
+    }
+}
\ No newline at end of file