Adds 'flinging-then-springing' to the PhysicsAnimator.

See http://go/physics-animator-design#heading=h.ib2uxhal5wrq and http://go/physics-animator-design#heading=h.fu8468xbcwxo

Test: atest SystemUITests
Change-Id: I55b0db822c9468de239dc5d2aa9fa080ad7131ab
This commit is contained in:
Joshua Tsuji
2019-12-16 17:03:08 -05:00
parent bdb6b20a8a
commit aaace7f27c
3 changed files with 364 additions and 66 deletions

View File

@@ -27,6 +27,9 @@ import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.systemui.util.animation.PhysicsAnimator.Companion.getInstance
import java.util.WeakHashMap
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
/**
* Extension function for all objects which will return a PhysicsAnimator instance for that object.
@@ -35,6 +38,15 @@ val <T : View> T.physicsAnimator: PhysicsAnimator<T> get() { return getInstance(
private const val TAG = "PhysicsAnimator"
private val UNSET = -Float.MAX_VALUE
/**
* [FlingAnimation] multiplies the friction set via [FlingAnimation.setFriction] by 4.2f, which is
* where this number comes from. We use it in [PhysicsAnimator.flingThenSpring] to calculate the
* minimum velocity for a fling to reach a certain value, given the fling's friction.
*/
private const val FLING_FRICTION_SCALAR_MULTIPLIER = 4.2f
typealias EndAction = () -> Unit
/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */
@@ -235,6 +247,71 @@ class PhysicsAnimator<T> private constructor (val target: T) {
return fling(property, startVelocity, config.friction, config.min, config.max)
}
/**
* Flings a property using the given start velocity. If the fling animation reaches the min/max
* bounds (from the [flingConfig]) with velocity remaining, it'll overshoot it and spring back.
*
* If the object is already out of the fling bounds, it will immediately spring back within
* bounds.
*
* This is useful for animating objects that are bounded by constraints such as screen edges,
* since otherwise the fling animation would end abruptly upon reaching the min/max bounds.
*
* @param property The property to animate.
* @param startVelocity The velocity, in pixels/second, with which to start the fling. If the
* object is already outside the fling bounds, this velocity will be used as the start velocity
* of the spring that will spring it back within bounds.
* @param flingMustReachMinOrMax If true, the fling animation is guaranteed to reach either its
* minimum bound (if [startVelocity] is negative) or maximum bound (if it's positive). The
* animator will use startVelocity if it's sufficient, or add more velocity if necessary. This
* is useful when fling's deceleration-based physics are preferable to the acceleration-based
* forces used by springs - typically, when you're allowing the user to move an object somewhere
* on the screen, but it needs to be along an edge.
* @param flingConfig The configuration to use for the fling portion of the animation.
* @param springConfig The configuration to use for the spring portion of the animation.
*/
@JvmOverloads
fun flingThenSpring(
property: FloatPropertyCompat<in T>,
startVelocity: Float,
flingConfig: FlingConfig,
springConfig: SpringConfig,
flingMustReachMinOrMax: Boolean = false
): PhysicsAnimator<T> {
val flingConfigCopy = flingConfig.copy()
val springConfigCopy = springConfig.copy()
val toAtLeast = if (startVelocity < 0) flingConfig.min else flingConfig.max
// If the fling needs to reach min/max, calculate the velocity required to do so and use
// that if the provided start velocity is not sufficient.
if (flingMustReachMinOrMax &&
toAtLeast != -Float.MAX_VALUE && toAtLeast != Float.MAX_VALUE) {
val distanceToDestination = toAtLeast - property.getValue(target)
// The minimum velocity required for the fling to end up at the given destination,
// taking the provided fling friction value.
val velocityToReachDestination = distanceToDestination *
(flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
// Try to use the provided start velocity, but use the required velocity to reach the
// destination if the provided velocity is insufficient.
val sufficientVelocity =
if (distanceToDestination < 0)
min(velocityToReachDestination, startVelocity)
else
max(velocityToReachDestination, startVelocity)
flingConfigCopy.startVelocity = sufficientVelocity
springConfigCopy.finalPosition = toAtLeast
} else {
flingConfigCopy.startVelocity = startVelocity
}
flingConfigs[property] = flingConfigCopy
springConfigs[property] = springConfigCopy
return this
}
/**
* Adds a listener that will be called whenever any property on the animated object is updated.
* This will be called on every animation frame, with the current value of the animated object
@@ -246,7 +323,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
}
/**
* Adds a listener that will be called whenever a property's animation ends. This is useful if
* Adds a listener that will be called when a property stops animating. This is useful if
* you care about a specific property ending, or want to use the end value/end velocity from a
* particular property's animation. If you just want to run an action when all property
* animations have ended, use [withEndActions].
@@ -311,6 +388,114 @@ class PhysicsAnimator<T> private constructor (val target: T) {
"your test setup.")
}
// Functions that will actually start the animations. These are run after we build and add
// the InternalListener, since some animations might update/end immediately and we don't
// want to miss those updates.
val animationStartActions = ArrayList<() -> Unit>()
for (animatedProperty in getAnimatedProperties()) {
val flingConfig = flingConfigs[animatedProperty]
val springConfig = springConfigs[animatedProperty]
// The property's current value on the object.
val currentValue = animatedProperty.getValue(target)
// Start by checking for a fling configuration. If one is present, we're either flinging
// or flinging-then-springing. Either way, we'll want to start the fling first.
if (flingConfig != null) {
animationStartActions.add {
// When the animation is starting, adjust the min/max bounds to include the
// current value of the property, if necessary. This is required to allow a
// fling to bring an out-of-bounds object back into bounds. For example, if an
// object was dragged halfway off the left side of the screen, but then flung to
// the right, we don't want the animation to end instantly just because the
// object started out of bounds. If the fling is in the direction that would
// take it farther out of bounds, it will end instantly as expected.
flingConfig.apply {
min = min(currentValue, this.min)
max = max(currentValue, this.max)
}
// Apply the configuration and start the animation.
getFlingAnimation(animatedProperty)
.also { flingConfig.applyToAnimation(it) }
.start()
}
}
// Check for a spring configuration. If one is present, we're either springing, or
// flinging-then-springing.
if (springConfig != null) {
// If there is no corresponding fling config, we're only springing.
if (flingConfig == null) {
// Apply the configuration and start the animation.
val springAnim = getSpringAnimation(animatedProperty)
springConfig.applyToAnimation(springAnim)
animationStartActions.add(springAnim::start)
} else {
// If there's a corresponding fling config, we're flinging-then-springing. Save
// the fling's original bounds so we can spring to them when the fling ends.
val flingMin = flingConfig.min
val flingMax = flingConfig.max
// Add an end listener that will start the spring when the fling ends.
endListeners.add(0, object : EndListener<T> {
override fun onAnimationEnd(
target: T,
property: FloatPropertyCompat<in T>,
wasFling: Boolean,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float,
allRelevantPropertyAnimsEnded: Boolean
) {
// If this isn't the relevant property, it wasn't a fling, or the fling
// was explicitly cancelled, don't spring.
if (property != animatedProperty || !wasFling || canceled) {
return
}
val endedWithVelocity = abs(finalVelocity) > 0
// If the object was out of bounds when the fling animation started, it
// will immediately end. In that case, we'll spring it back in bounds.
val endedOutOfBounds = finalValue !in flingMin..flingMax
// If the fling ended either out of bounds or with remaining velocity,
// it's time to spring.
if (endedWithVelocity || endedOutOfBounds) {
springConfig.startVelocity = finalVelocity
// If the spring's final position isn't set, this is a
// flingThenSpring where flingMustReachMinOrMax was false. We'll
// need to set the spring's final position here.
if (springConfig.finalPosition == UNSET) {
if (endedWithVelocity) {
// If the fling ended with negative velocity, that means it
// hit the min bound, so spring to that bound (and vice
// versa).
springConfig.finalPosition =
if (finalVelocity < 0) flingMin else flingMax
} else if (endedOutOfBounds) {
// If the fling ended out of bounds, spring it to the
// nearest bound.
springConfig.finalPosition =
if (finalValue < flingMin) flingMin else flingMax
}
}
// Apply the configuration and start the spring animation.
getSpringAnimation(animatedProperty)
.also { springConfig.applyToAnimation(it) }
.start()
}
}
})
}
}
}
// Add an internal listener that will dispatch animation events to the provided listeners.
internalListeners.add(InternalListener(
getAnimatedProperties(),
@@ -318,24 +503,10 @@ class PhysicsAnimator<T> private constructor (val target: T) {
ArrayList(endListeners),
ArrayList(endActions)))
for ((property, config) in flingConfigs) {
val currentValue = property.getValue(target)
// If the fling is already out of bounds, don't start it.
if (currentValue <= config.min || currentValue >= config.max) {
continue
}
val flingAnim = getFlingAnimation(property)
config.applyToAnimation(flingAnim)
flingAnim.start()
}
for ((property, config) in springConfigs) {
val springAnim = getSpringAnimation(property)
config.applyToAnimation(springAnim)
springAnim.start()
}
// Actually start the DynamicAnimations. This is delayed until after the InternalListener is
// constructed and added so that we don't miss the end listener firing for any animations
// that immediately end.
animationStartActions.forEach { it.invoke() }
clearAnimator()
}
@@ -381,7 +552,10 @@ class PhysicsAnimator<T> private constructor (val target: T) {
}
anim.addEndListener { _, canceled, value, velocity ->
internalListeners.removeAll {
it.onInternalAnimationEnd(property, canceled, value, velocity) } }
it.onInternalAnimationEnd(
property, canceled, value, velocity, anim is FlingAnimation)
}
}
return anim
}
@@ -434,7 +608,8 @@ class PhysicsAnimator<T> private constructor (val target: T) {
property: FloatPropertyCompat<in T>,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float
finalVelocity: Float,
isFling: Boolean
): Boolean {
// If this property animation isn't relevant to this listener, ignore it.
@@ -461,7 +636,15 @@ class PhysicsAnimator<T> private constructor (val target: T) {
val allEnded = !arePropertiesAnimating(properties)
endListeners.forEach {
it.onAnimationEnd(target, property, canceled, finalValue, finalVelocity, allEnded) }
it.onAnimationEnd(
target, property, isFling, canceled, finalValue, finalVelocity,
allEnded)
// Check that the end listener didn't restart this property's animation.
if (isPropertyAnimating(property)) {
return false
}
}
// If all of the animations that this listener cares about have ended, run the end
// actions unless the animation was canceled.
@@ -524,15 +707,15 @@ class PhysicsAnimator<T> private constructor (val target: T) {
data class SpringConfig internal constructor(
internal var stiffness: Float,
internal var dampingRatio: Float,
internal var startVel: Float = 0f,
internal var finalPosition: Float = -Float.MAX_VALUE
internal var startVelocity: Float = 0f,
internal var finalPosition: Float = UNSET
) {
constructor() :
this(defaultSpring.stiffness, defaultSpring.dampingRatio)
constructor(stiffness: Float, dampingRatio: Float) :
this(stiffness = stiffness, dampingRatio = dampingRatio, startVel = 0f)
this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f)
/** Apply these configuration settings to the given SpringAnimation. */
internal fun applyToAnimation(anim: SpringAnimation) {
@@ -543,7 +726,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
finalPosition = this@SpringConfig.finalPosition
}
if (startVel != 0f) anim.setStartVelocity(startVel)
if (startVelocity != 0f) anim.setStartVelocity(startVelocity)
}
}
@@ -556,7 +739,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
internal var friction: Float,
internal var min: Float,
internal var max: Float,
internal var startVel: Float
internal var startVelocity: Float
) {
constructor() : this(defaultFling.friction)
@@ -565,7 +748,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
this(friction, defaultFling.min, defaultFling.max)
constructor(friction: Float, min: Float, max: Float) :
this(friction, min, max, startVel = 0f)
this(friction, min, max, startVelocity = 0f)
/** Apply these configuration settings to the given FlingAnimation. */
internal fun applyToAnimation(anim: FlingAnimation) {
@@ -573,7 +756,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
friction = this@FlingConfig.friction
setMinValue(min)
setMaxValue(max)
setStartVelocity(startVel)
setStartVelocity(startVelocity)
}
}
}
@@ -626,6 +809,10 @@ class PhysicsAnimator<T> private constructor (val target: T) {
*
* @param target The animated object itself.
* @param property The property whose animation has just ended.
* @param wasFling Whether this property ended after a fling animation (as opposed to a
* spring animation). If this property was animated via [flingThenSpring], this will be true
* if the fling animation did not reach the min/max bounds, decelerating to a stop
* naturally. It will be false if it hit the bounds and was sprung back.
* @param canceled Whether the animation was explicitly canceled before it naturally ended.
* @param finalValue The final value of the animated property.
* @param finalVelocity The final velocity (in pixels per second) of the ended animation.
@@ -663,6 +850,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
fun onAnimationEnd(
target: T,
property: FloatPropertyCompat<in T>,
wasFling: Boolean,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float,

View File

@@ -19,6 +19,7 @@ import android.os.Handler
import android.os.Looper
import android.util.ArrayMap
import androidx.dynamicanimation.animation.FloatPropertyCompat
import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.prepareForTest
import java.util.ArrayDeque
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -119,6 +120,7 @@ object PhysicsAnimatorTestUtils {
override fun onAnimationEnd(
target: T,
property: FloatPropertyCompat<in T>,
wasFling: Boolean,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float,
@@ -389,8 +391,6 @@ object PhysicsAnimatorTestUtils {
val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1)
animationThreadHandler.post {
val animatedProperties = animator.getAnimatedProperties()
// Add an update listener that dispatches to any test update listeners added by
// tests.
animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> {
@@ -398,6 +398,10 @@ object PhysicsAnimatorTestUtils {
target: T,
values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
) {
values.forEach { (property, value) ->
allUpdates.getOrPut(property, { ArrayList() }).add(value)
}
for (listener in testUpdateListeners) {
listener.onAnimationUpdateForProperty(target, values)
}
@@ -410,6 +414,7 @@ object PhysicsAnimatorTestUtils {
override fun onAnimationEnd(
target: T,
property: FloatPropertyCompat<in T>,
wasFling: Boolean,
canceled: Boolean,
finalValue: Float,
finalVelocity: Float,
@@ -417,7 +422,7 @@ object PhysicsAnimatorTestUtils {
) {
for (listener in testEndListeners) {
listener.onAnimationEnd(
target, property, canceled, finalValue, finalVelocity,
target, property, wasFling, canceled, finalValue, finalVelocity,
allRelevantPropertyAnimsEnded)
}
@@ -432,31 +437,6 @@ object PhysicsAnimatorTestUtils {
}
})
val updateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>().also {
it.add(object : PhysicsAnimator.UpdateListener<T> {
override fun onAnimationUpdateForProperty(
target: T,
values: ArrayMap<FloatPropertyCompat<in T>,
PhysicsAnimator.AnimationUpdate>
) {
values.forEach { (property, value) ->
allUpdates.getOrPut(property, { ArrayList() }).add(value)
}
}
})
}
/**
* Add an internal listener at the head of the list that captures update values
* directly from DynamicAnimation. We use this to build a list of all updates so we
* can verify that InternalListener dispatches to the real listeners properly.
*/
animator.internalListeners.add(0, animator.InternalListener(
animatedProperties,
updateListeners,
ArrayList(),
ArrayList()))
animator.startInternal()
unblockLatch.countDown()
}

View File

@@ -155,6 +155,7 @@ class PhysicsAnimatorTest : SysuiTestCase() {
verify(mockEndListener).onAnimationEnd(
testView,
DynamicAnimation.TRANSLATION_X,
wasFling = false,
canceled = false,
finalValue = 10f,
finalVelocity = 0f,
@@ -176,6 +177,7 @@ class PhysicsAnimatorTest : SysuiTestCase() {
verify(mockEndListener).onAnimationEnd(
testView,
DynamicAnimation.TRANSLATION_Y,
wasFling = false,
canceled = false,
finalValue = 500f,
finalVelocity = 0f,
@@ -215,8 +217,8 @@ class PhysicsAnimatorTest : SysuiTestCase() {
verifyUpdateListenerCalls(animator, mockUpdateListener)
verify(mockEndListener, times(1)).onAnimationEnd(
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), anyFloat(), anyFloat(),
eq(true))
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(false), anyFloat(),
anyFloat(), eq(true))
verify(mockEndAction, times(1)).run()
animator
@@ -330,13 +332,24 @@ class PhysicsAnimatorTest : SysuiTestCase() {
// The original end listener should also have been called, with allEnded = true since it was
// provided to an animator that animated only TRANSLATION_X.
verify(mockEndListener, times(1))
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, false, 200f, 0f, true)
.onAnimationEnd(
testView, DynamicAnimation.TRANSLATION_X,
wasFling = false,
canceled = false,
finalValue = 200f,
finalVelocity = 0f,
allRelevantPropertyAnimsEnded = true)
verifyNoMoreInteractions(mockEndListener)
// The second end listener should have been called, but with allEnded = false since it was
// provided to an animator that animated both TRANSLATION_X and TRANSLATION_Y.
verify(secondEndListener, times(1))
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, false, 200f, 0f, false)
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X,
wasFling = false,
canceled = false,
finalValue = 200f,
finalVelocity = 0f,
allRelevantPropertyAnimsEnded = false)
verifyNoMoreInteractions(secondEndListener)
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y)
@@ -346,7 +359,12 @@ class PhysicsAnimatorTest : SysuiTestCase() {
verifyNoMoreInteractions(mockEndListener)
verify(secondEndListener, times(1))
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y, false, 4000f, 0f, true)
.onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y,
wasFling = false,
canceled = false,
finalValue = 4000f,
finalVelocity = 0f,
allRelevantPropertyAnimsEnded = true)
verifyNoMoreInteractions(secondEndListener)
}
@@ -365,8 +383,8 @@ class PhysicsAnimatorTest : SysuiTestCase() {
assertEquals(10f, testView.translationX, 1f)
verify(mockEndListener, times(1))
.onAnimationEnd(
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(10f),
anyFloat(), eq(true))
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false),
eq(10f), anyFloat(), eq(true))
animator
.fling(
@@ -382,8 +400,8 @@ class PhysicsAnimatorTest : SysuiTestCase() {
assertEquals(-5f, testView.translationX, 1f)
verify(mockEndListener, times(1))
.onAnimationEnd(
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(-5f),
anyFloat(), eq(true))
eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false),
eq(-5f), anyFloat(), eq(true))
}
@Test
@@ -427,6 +445,118 @@ class PhysicsAnimatorTest : SysuiTestCase() {
assertEquals(200f, testView.translationX, 1f)
}
@Test
fun testFlingThenSpring() {
PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
// Start at 500f and fling hard to the left. We should quickly reach the 250f minimum, fly
// past it since there's so much velocity remaining, then spring back to 250f.
testView.translationX = 500f
animator
.flingThenSpring(
DynamicAnimation.TRANSLATION_X,
-5000f,
flingConfig.apply { min = 250f },
springConfig)
.addUpdateListener(mockUpdateListener)
.addEndListener(mockEndListener)
.withEndActions(mockEndAction::run)
.start()
// Block until we pass the minimum.
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(
animator) { v -> v.translationX <= 250f }
// Double check that the view is there.
assertTrue(testView.translationX <= 250f)
// The update listener should have been called with a value < 500f, and then a value less
// than or equal to the 250f minimum.
verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
{ u -> u.value < 500f },
{ u -> u.value <= 250f })
// Despite the fact that the fling has ended, the end listener shouldn't have been called
// since we're about to begin springing the same property.
verifyNoMoreInteractions(mockEndListener)
verifyNoMoreInteractions(mockEndAction)
// Wait for the spring to finish.
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_X)
// Make sure we continued past 250f since the spring should have been started with some
// remaining negative velocity from the fling.
verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
{ u -> u.value < 250f })
// At this point, the animation end listener should have been called once, and only once,
// when the spring ended at 250f.
verify(mockEndListener).onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X,
wasFling = false,
canceled = false,
finalValue = 250f,
finalVelocity = 0f,
allRelevantPropertyAnimsEnded = true)
verifyNoMoreInteractions(mockEndListener)
// The end action should also have been called once.
verify(mockEndAction, times(1)).run()
verifyNoMoreInteractions(mockEndAction)
assertEquals(250f, testView.translationX)
}
@Test
fun testFlingThenSpring_objectOutsideFlingBounds() {
// Start the view at x = -500, well outside the fling bounds of min = 0f, with strong
// negative velocity.
testView.translationX = -500f
animator
.flingThenSpring(
DynamicAnimation.TRANSLATION_X,
-5000f,
flingConfig.apply { min = 0f },
springConfig)
.addUpdateListener(mockUpdateListener)
.addEndListener(mockEndListener)
.withEndActions(mockEndAction::run)
.start()
// The initial -5000f velocity should result in frames to the left of -500f before the view
// springs back towards 0f.
verifyAnimationUpdateFrames(
animator, DynamicAnimation.TRANSLATION_X,
{ u -> u.value < -500f },
{ u -> u.value > -500f })
// We should end up at the fling min.
assertEquals(0f, testView.translationX, 1f)
}
@Test
fun testFlingToMinMaxThenSpring() {
// Start at x = 500f.
testView.translationX = 500f
// Fling to the left at the very sad rate of -1 pixels per second. That won't get us much of
// anywhere, and certainly not to the 0f min.
animator
// Good thing we have flingToMinMaxThenSpring!
.flingThenSpring(
DynamicAnimation.TRANSLATION_X,
-10000f,
flingConfig.apply { min = 0f },
springConfig,
flingMustReachMinOrMax = true)
.addUpdateListener(mockUpdateListener)
.addEndListener(mockEndListener)
.withEndActions(mockEndAction::run)
.start()
// Thanks, flingToMinMaxThenSpring, for adding enough velocity to get us here.
assertEquals(0f, testView.translationX, 1f)
}
/**
* Verifies that the calls to the mock update listener match the animation update frames
* reported by the test internal listener, in order.