blob: 8625d63a3c7ef91180c6f9daa24aa179e2c304e0 [file] [log] [blame]
Joshua Tsuji408b9592019-11-07 18:32:58 -05001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.util.animation
18
19import android.os.Looper
20import android.util.ArrayMap
21import android.util.Log
22import android.view.View
23import androidx.dynamicanimation.animation.DynamicAnimation
24import androidx.dynamicanimation.animation.FlingAnimation
25import androidx.dynamicanimation.animation.FloatPropertyCompat
26import androidx.dynamicanimation.animation.SpringAnimation
27import androidx.dynamicanimation.animation.SpringForce
28import com.android.systemui.util.animation.PhysicsAnimator.Companion.getInstance
29import java.util.WeakHashMap
Joshua Tsujiaaace7f2019-12-16 17:03:08 -050030import kotlin.math.abs
31import kotlin.math.max
32import kotlin.math.min
Joshua Tsuji408b9592019-11-07 18:32:58 -050033
34/**
35 * Extension function for all objects which will return a PhysicsAnimator instance for that object.
36 */
37val <T : View> T.physicsAnimator: PhysicsAnimator<T> get() { return getInstance(this) }
38
39private const val TAG = "PhysicsAnimator"
40
Joshua Tsujiaaace7f2019-12-16 17:03:08 -050041private val UNSET = -Float.MAX_VALUE
42
43/**
44 * [FlingAnimation] multiplies the friction set via [FlingAnimation.setFriction] by 4.2f, which is
45 * where this number comes from. We use it in [PhysicsAnimator.flingThenSpring] to calculate the
46 * minimum velocity for a fling to reach a certain value, given the fling's friction.
47 */
48private const val FLING_FRICTION_SCALAR_MULTIPLIER = 4.2f
49
Joshua Tsuji408b9592019-11-07 18:32:58 -050050typealias EndAction = () -> Unit
51
52/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */
53typealias UpdateMap<T> =
54 ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
55
56/**
57 * Map of the animators associated with a given object. This ensures that only one animator
58 * per object exists.
59 */
60internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>()
61
62/**
63 * Default spring configuration to use for animations where stiffness and/or damping ratio
64 * were not provided.
65 */
66private val defaultSpring = PhysicsAnimator.SpringConfig(
67 SpringForce.STIFFNESS_MEDIUM,
68 SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
69
70/** Default fling configuration to use for animations where friction was not provided. */
71private val defaultFling = PhysicsAnimator.FlingConfig(
72 friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE)
73
74/** Whether to log helpful debug information about animations. */
75private var verboseLogging = false
76
77/**
78 * Animator that uses physics-based animations to animate properties on views and objects. Physics
79 * animations use real-world physical concepts, such as momentum and mass, to realistically simulate
80 * motion. PhysicsAnimator is heavily inspired by [android.view.ViewPropertyAnimator], and
81 * also uses the builder pattern to configure and start animations.
82 *
83 * The physics animations are backed by [DynamicAnimation].
84 *
85 * @param T The type of the object being animated.
86 */
87class PhysicsAnimator<T> private constructor (val target: T) {
88
89 /** Data class for representing animation frame updates. */
90 data class AnimationUpdate(val value: Float, val velocity: Float)
91
92 /** [DynamicAnimation] instances for the given properties. */
93 private val springAnimations = ArrayMap<FloatPropertyCompat<in T>, SpringAnimation>()
94 private val flingAnimations = ArrayMap<FloatPropertyCompat<in T>, FlingAnimation>()
95
96 /**
97 * Spring and fling configurations for the properties to be animated on the target. We'll
98 * configure and start the DynamicAnimations for these properties according to the provided
99 * configurations.
100 */
101 private val springConfigs = ArrayMap<FloatPropertyCompat<in T>, SpringConfig>()
102 private val flingConfigs = ArrayMap<FloatPropertyCompat<in T>, FlingConfig>()
103
104 /**
105 * Animation listeners for the animation. These will be notified when each property animation
106 * updates or ends.
107 */
108 private val updateListeners = ArrayList<UpdateListener<T>>()
109 private val endListeners = ArrayList<EndListener<T>>()
110
111 /** End actions to run when all animations have completed. */
112 private val endActions = ArrayList<EndAction>()
113
114 /**
115 * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to
116 * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add
117 * just one permanent update and end listener to the DynamicAnimations.
118 */
119 internal var internalListeners = ArrayList<InternalListener>()
120
121 /**
122 * Action to run when [start] is called. This can be changed by
123 * [PhysicsAnimatorTestUtils.prepareForTest] to enable animators to run under test and provide
124 * helpful test utilities.
125 */
126 internal var startAction: () -> Unit = ::startInternal
127
128 /**
Joshua Tsuji959ac762020-02-13 03:21:56 -0500129 * Action to run when [cancel] is called. This can be changed by
130 * [PhysicsAnimatorTestUtils.prepareForTest] to cancel animations from the main thread, which
131 * is required.
132 */
133 internal var cancelAction: (Set<FloatPropertyCompat<in T>>) -> Unit = ::cancelInternal
134
135 /**
Joshua Tsuji408b9592019-11-07 18:32:58 -0500136 * Springs a property to the given value, using the provided configuration settings.
137 *
138 * Springs are used when you know the exact value to which you want to animate. They can be
139 * configured with a start velocity (typically used when the spring is initiated by a touch
140 * event), but this velocity will be realistically attenuated as forces are applied to move the
141 * property towards the end value.
142 *
143 * If you find yourself repeating the same stiffness and damping ratios many times, consider
144 * storing a single [SpringConfig] instance and passing that in instead of individual values.
145 *
146 * @param property The property to spring to the given value. The property must be an instance
147 * of FloatPropertyCompat&lt;? super T&gt;. For example, if this is a
148 * PhysicsAnimator&lt;FrameLayout&gt;, you can use a FloatPropertyCompat&lt;FrameLayout&gt;, as
149 * well as a FloatPropertyCompat&lt;ViewGroup&gt;, and so on.
150 * @param toPosition The value to spring the given property to.
151 * @param startVelocity The initial velocity to use for the animation.
152 * @param stiffness The stiffness to use for the spring. Higher stiffness values result in
153 * faster animations, while lower stiffness means a slower animation. Reasonable values for
154 * low, medium, and high stiffness can be found as constants in [SpringForce].
155 * @param dampingRatio The damping ratio (bounciness) to use for the spring. Higher values
156 * result in a less 'springy' animation, while lower values allow the animation to bounce
157 * back and forth for a longer time after reaching the final position. Reasonable values for
158 * low, medium, and high damping can be found in [SpringForce].
159 */
160 fun spring(
161 property: FloatPropertyCompat<in T>,
162 toPosition: Float,
163 startVelocity: Float = 0f,
164 stiffness: Float = defaultSpring.stiffness,
165 dampingRatio: Float = defaultSpring.dampingRatio
166 ): PhysicsAnimator<T> {
167 if (verboseLogging) {
168 Log.d(TAG, "Springing ${getReadablePropertyName(property)} to $toPosition.")
169 }
170
171 springConfigs[property] =
172 SpringConfig(stiffness, dampingRatio, startVelocity, toPosition)
173 return this
174 }
175
176 /**
177 * Springs a property to a given value using the provided start velocity and configuration
178 * options.
179 *
180 * @see spring
181 */
182 fun spring(
183 property: FloatPropertyCompat<in T>,
184 toPosition: Float,
185 startVelocity: Float,
186 config: SpringConfig = defaultSpring
187 ): PhysicsAnimator<T> {
188 return spring(
189 property, toPosition, startVelocity, config.stiffness, config.dampingRatio)
190 }
191
192 /**
193 * Springs a property to a given value using the provided configuration options, and a start
194 * velocity of 0f.
195 *
196 * @see spring
197 */
198 fun spring(
199 property: FloatPropertyCompat<in T>,
200 toPosition: Float,
201 config: SpringConfig = defaultSpring
202 ): PhysicsAnimator<T> {
203 return spring(property, toPosition, 0f, config)
204 }
205
206 /**
207 * Flings a property using the given start velocity, using a [FlingAnimation] configured using
208 * the provided configuration settings.
209 *
210 * Flings are used when you have a start velocity, and want the property value to realistically
211 * decrease as friction is applied until the velocity reaches zero. Flings do not have a
212 * deterministic end value. If you are attempting to animate to a specific end value, use
213 * [spring].
214 *
215 * If you find yourself repeating the same friction/min/max values, consider storing a single
216 * [FlingConfig] and passing that in instead.
217 *
218 * @param property The property to fling using the given start velocity.
219 * @param startVelocity The start velocity (in pixels per second) with which to start the fling.
220 * @param friction Friction value applied to slow down the animation over time. Higher values
221 * will more quickly slow the animation. Typical friction values range from 1f to 10f.
222 * @param min The minimum value allowed for the animation. If this value is reached, the
223 * animation will end abruptly.
224 * @param max The maximum value allowed for the animation. If this value is reached, the
225 * animation will end abruptly.
226 */
227 fun fling(
228 property: FloatPropertyCompat<in T>,
229 startVelocity: Float,
230 friction: Float = defaultFling.friction,
231 min: Float = defaultFling.min,
232 max: Float = defaultFling.max
233 ): PhysicsAnimator<T> {
234 if (verboseLogging) {
235 Log.d(TAG, "Flinging ${getReadablePropertyName(property)} " +
236 "with velocity $startVelocity.")
237 }
238
239 flingConfigs[property] = FlingConfig(friction, min, max, startVelocity)
240 return this
241 }
242
243 /**
244 * Flings a property using the given start velocity, using a [FlingAnimation] configured using
245 * the provided configuration settings.
246 *
247 * @see fling
248 */
249 fun fling(
250 property: FloatPropertyCompat<in T>,
251 startVelocity: Float,
252 config: FlingConfig = defaultFling
253 ): PhysicsAnimator<T> {
254 return fling(property, startVelocity, config.friction, config.min, config.max)
255 }
256
257 /**
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500258 * Flings a property using the given start velocity. If the fling animation reaches the min/max
259 * bounds (from the [flingConfig]) with velocity remaining, it'll overshoot it and spring back.
260 *
261 * If the object is already out of the fling bounds, it will immediately spring back within
262 * bounds.
263 *
264 * This is useful for animating objects that are bounded by constraints such as screen edges,
265 * since otherwise the fling animation would end abruptly upon reaching the min/max bounds.
266 *
267 * @param property The property to animate.
268 * @param startVelocity The velocity, in pixels/second, with which to start the fling. If the
269 * object is already outside the fling bounds, this velocity will be used as the start velocity
270 * of the spring that will spring it back within bounds.
271 * @param flingMustReachMinOrMax If true, the fling animation is guaranteed to reach either its
272 * minimum bound (if [startVelocity] is negative) or maximum bound (if it's positive). The
273 * animator will use startVelocity if it's sufficient, or add more velocity if necessary. This
274 * is useful when fling's deceleration-based physics are preferable to the acceleration-based
275 * forces used by springs - typically, when you're allowing the user to move an object somewhere
276 * on the screen, but it needs to be along an edge.
277 * @param flingConfig The configuration to use for the fling portion of the animation.
278 * @param springConfig The configuration to use for the spring portion of the animation.
279 */
280 @JvmOverloads
281 fun flingThenSpring(
282 property: FloatPropertyCompat<in T>,
283 startVelocity: Float,
284 flingConfig: FlingConfig,
285 springConfig: SpringConfig,
286 flingMustReachMinOrMax: Boolean = false
287 ): PhysicsAnimator<T> {
288 val flingConfigCopy = flingConfig.copy()
289 val springConfigCopy = springConfig.copy()
290 val toAtLeast = if (startVelocity < 0) flingConfig.min else flingConfig.max
291
292 // If the fling needs to reach min/max, calculate the velocity required to do so and use
293 // that if the provided start velocity is not sufficient.
294 if (flingMustReachMinOrMax &&
295 toAtLeast != -Float.MAX_VALUE && toAtLeast != Float.MAX_VALUE) {
296 val distanceToDestination = toAtLeast - property.getValue(target)
297
298 // The minimum velocity required for the fling to end up at the given destination,
299 // taking the provided fling friction value.
300 val velocityToReachDestination = distanceToDestination *
301 (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
302
Joshua Tsujia2653992020-01-15 14:28:38 -0500303 // If there's distance to cover, and the provided velocity is moving in the correct
304 // direction, ensure that the velocity is high enough to reach the destination.
305 // Otherwise, just use startVelocity - this means that the fling is at or out of bounds.
306 // The fling will immediately end and a spring will bring the object back into bounds
307 // with this startVelocity.
308 flingConfigCopy.startVelocity = when {
309 distanceToDestination > 0f && startVelocity >= 0f ->
310 max(velocityToReachDestination, startVelocity)
311 distanceToDestination < 0f && startVelocity <= 0f ->
312 min(velocityToReachDestination, startVelocity)
313 else -> startVelocity
314 }
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500315
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500316 springConfigCopy.finalPosition = toAtLeast
317 } else {
318 flingConfigCopy.startVelocity = startVelocity
319 }
320
321 flingConfigs[property] = flingConfigCopy
322 springConfigs[property] = springConfigCopy
323 return this
324 }
325
326 /**
Joshua Tsuji408b9592019-11-07 18:32:58 -0500327 * Adds a listener that will be called whenever any property on the animated object is updated.
328 * This will be called on every animation frame, with the current value of the animated object
329 * and the new property values.
330 */
331 fun addUpdateListener(listener: UpdateListener<T>): PhysicsAnimator<T> {
332 updateListeners.add(listener)
333 return this
334 }
335
336 /**
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500337 * Adds a listener that will be called when a property stops animating. This is useful if
Joshua Tsuji408b9592019-11-07 18:32:58 -0500338 * you care about a specific property ending, or want to use the end value/end velocity from a
339 * particular property's animation. If you just want to run an action when all property
340 * animations have ended, use [withEndActions].
341 */
342 fun addEndListener(listener: EndListener<T>): PhysicsAnimator<T> {
343 endListeners.add(listener)
344 return this
345 }
346
347 /**
348 * Adds end actions that will be run sequentially when animations for every property involved in
349 * this specific animation have ended (unless they were explicitly canceled). For example, if
350 * you call:
351 *
352 * animator
353 * .spring(TRANSLATION_X, ...)
354 * .spring(TRANSLATION_Y, ...)
355 * .withEndAction(action)
356 * .start()
357 *
358 * 'action' will be run when both TRANSLATION_X and TRANSLATION_Y end.
359 *
360 * Other properties may still be animating, if those animations were not started in the same
361 * call. For example:
362 *
363 * animator
364 * .spring(ALPHA, ...)
365 * .start()
366 *
367 * animator
368 * .spring(TRANSLATION_X, ...)
369 * .spring(TRANSLATION_Y, ...)
370 * .withEndAction(action)
371 * .start()
372 *
373 * 'action' will still be run as soon as TRANSLATION_X and TRANSLATION_Y end, even if ALPHA is
374 * still animating.
375 *
376 * If you want to run actions as soon as a subset of property animations have ended, you want
377 * access to the animation's end value/velocity, or you want to run these actions even if the
378 * animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param,
379 * which indicates that all relevant animations have ended.
380 */
Joshua Tsujibace2242020-01-14 15:51:48 -0500381 fun withEndActions(vararg endActions: EndAction?): PhysicsAnimator<T> {
382 this.endActions.addAll(endActions.filterNotNull())
383 return this
384 }
385
386 /**
387 * Helper overload so that callers from Java can use Runnables or method references as end
388 * actions without having to explicitly return Unit.
389 */
390 fun withEndActions(vararg endActions: Runnable?): PhysicsAnimator<T> {
391 this.endActions.addAll(endActions.filterNotNull().map { it::run })
Joshua Tsuji408b9592019-11-07 18:32:58 -0500392 return this
393 }
394
395 /** Starts the animations! */
396 fun start() {
397 startAction()
398 }
399
400 /**
401 * Starts the animations for real! This is typically called immediately by [start] unless this
402 * animator is under test.
403 */
404 internal fun startInternal() {
405 if (!Looper.getMainLooper().isCurrentThread) {
406 Log.e(TAG, "Animations can only be started on the main thread. If you are seeing " +
407 "this message in a test, call PhysicsAnimatorTestUtils#prepareForTest in " +
408 "your test setup.")
409 }
410
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500411 // Functions that will actually start the animations. These are run after we build and add
412 // the InternalListener, since some animations might update/end immediately and we don't
413 // want to miss those updates.
414 val animationStartActions = ArrayList<() -> Unit>()
415
416 for (animatedProperty in getAnimatedProperties()) {
417 val flingConfig = flingConfigs[animatedProperty]
418 val springConfig = springConfigs[animatedProperty]
419
420 // The property's current value on the object.
421 val currentValue = animatedProperty.getValue(target)
422
423 // Start by checking for a fling configuration. If one is present, we're either flinging
424 // or flinging-then-springing. Either way, we'll want to start the fling first.
425 if (flingConfig != null) {
426 animationStartActions.add {
427 // When the animation is starting, adjust the min/max bounds to include the
428 // current value of the property, if necessary. This is required to allow a
429 // fling to bring an out-of-bounds object back into bounds. For example, if an
430 // object was dragged halfway off the left side of the screen, but then flung to
431 // the right, we don't want the animation to end instantly just because the
432 // object started out of bounds. If the fling is in the direction that would
433 // take it farther out of bounds, it will end instantly as expected.
434 flingConfig.apply {
435 min = min(currentValue, this.min)
436 max = max(currentValue, this.max)
437 }
438
Joshua Tsuji959ac762020-02-13 03:21:56 -0500439 // Flings can't be updated to a new position while maintaining velocity, because
440 // we're using the explicitly provided start velocity. Cancel any flings (or
441 // springs) on this property before flinging.
442 cancel(animatedProperty)
443
444 // Apply the configuration and start the animation.
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500445 getFlingAnimation(animatedProperty)
446 .also { flingConfig.applyToAnimation(it) }
447 .start()
448 }
449 }
450
451 // Check for a spring configuration. If one is present, we're either springing, or
452 // flinging-then-springing.
453 if (springConfig != null) {
454
455 // If there is no corresponding fling config, we're only springing.
456 if (flingConfig == null) {
457 // Apply the configuration and start the animation.
458 val springAnim = getSpringAnimation(animatedProperty)
459 springConfig.applyToAnimation(springAnim)
460 animationStartActions.add(springAnim::start)
461 } else {
462 // If there's a corresponding fling config, we're flinging-then-springing. Save
463 // the fling's original bounds so we can spring to them when the fling ends.
464 val flingMin = flingConfig.min
465 val flingMax = flingConfig.max
466
467 // Add an end listener that will start the spring when the fling ends.
468 endListeners.add(0, object : EndListener<T> {
469 override fun onAnimationEnd(
470 target: T,
471 property: FloatPropertyCompat<in T>,
472 wasFling: Boolean,
473 canceled: Boolean,
474 finalValue: Float,
475 finalVelocity: Float,
476 allRelevantPropertyAnimsEnded: Boolean
477 ) {
478 // If this isn't the relevant property, it wasn't a fling, or the fling
479 // was explicitly cancelled, don't spring.
480 if (property != animatedProperty || !wasFling || canceled) {
481 return
482 }
483
484 val endedWithVelocity = abs(finalVelocity) > 0
485
486 // If the object was out of bounds when the fling animation started, it
487 // will immediately end. In that case, we'll spring it back in bounds.
488 val endedOutOfBounds = finalValue !in flingMin..flingMax
489
490 // If the fling ended either out of bounds or with remaining velocity,
491 // it's time to spring.
492 if (endedWithVelocity || endedOutOfBounds) {
493 springConfig.startVelocity = finalVelocity
494
495 // If the spring's final position isn't set, this is a
496 // flingThenSpring where flingMustReachMinOrMax was false. We'll
497 // need to set the spring's final position here.
498 if (springConfig.finalPosition == UNSET) {
499 if (endedWithVelocity) {
500 // If the fling ended with negative velocity, that means it
501 // hit the min bound, so spring to that bound (and vice
502 // versa).
503 springConfig.finalPosition =
504 if (finalVelocity < 0) flingMin else flingMax
505 } else if (endedOutOfBounds) {
506 // If the fling ended out of bounds, spring it to the
507 // nearest bound.
508 springConfig.finalPosition =
509 if (finalValue < flingMin) flingMin else flingMax
510 }
511 }
512
513 // Apply the configuration and start the spring animation.
514 getSpringAnimation(animatedProperty)
515 .also { springConfig.applyToAnimation(it) }
516 .start()
517 }
518 }
519 })
520 }
521 }
522 }
523
Joshua Tsuji408b9592019-11-07 18:32:58 -0500524 // Add an internal listener that will dispatch animation events to the provided listeners.
525 internalListeners.add(InternalListener(
526 getAnimatedProperties(),
527 ArrayList(updateListeners),
528 ArrayList(endListeners),
529 ArrayList(endActions)))
530
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500531 // Actually start the DynamicAnimations. This is delayed until after the InternalListener is
532 // constructed and added so that we don't miss the end listener firing for any animations
533 // that immediately end.
534 animationStartActions.forEach { it.invoke() }
Joshua Tsuji408b9592019-11-07 18:32:58 -0500535
536 clearAnimator()
537 }
538
539 /** Clear the animator's builder variables. */
540 private fun clearAnimator() {
541 springConfigs.clear()
542 flingConfigs.clear()
543
544 updateListeners.clear()
545 endListeners.clear()
546 endActions.clear()
547 }
548
549 /** Retrieves a spring animation for the given property, building one if needed. */
550 private fun getSpringAnimation(property: FloatPropertyCompat<in T>): SpringAnimation {
551 return springAnimations.getOrPut(
552 property,
553 { configureDynamicAnimation(SpringAnimation(target, property), property)
554 as SpringAnimation })
555 }
556
557 /** Retrieves a fling animation for the given property, building one if needed. */
558 private fun getFlingAnimation(property: FloatPropertyCompat<in T>): FlingAnimation {
559 return flingAnimations.getOrPut(
560 property,
561 { configureDynamicAnimation(FlingAnimation(target, property), property)
562 as FlingAnimation })
563 }
564
565 /**
566 * Adds update and end listeners to the DynamicAnimation which will dispatch to the internal
567 * listeners.
568 */
569 private fun configureDynamicAnimation(
570 anim: DynamicAnimation<*>,
571 property: FloatPropertyCompat<in T>
572 ): DynamicAnimation<*> {
573 anim.addUpdateListener { _, value, velocity ->
574 for (i in 0 until internalListeners.size) {
575 internalListeners[i].onInternalAnimationUpdate(property, value, velocity)
576 }
577 }
578 anim.addEndListener { _, canceled, value, velocity ->
579 internalListeners.removeAll {
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500580 it.onInternalAnimationEnd(
581 property, canceled, value, velocity, anim is FlingAnimation)
582 }
583 }
Joshua Tsuji408b9592019-11-07 18:32:58 -0500584 return anim
585 }
586
587 /**
588 * Internal listener class that receives updates from DynamicAnimation listeners, and dispatches
589 * them to the appropriate update/end listeners. This class is also aware of which properties
590 * were being animated when the end listeners were passed in, so that we can provide the
591 * appropriate value for allEnded to [EndListener.onAnimationEnd].
592 */
593 internal inner class InternalListener constructor(
594 private var properties: Set<FloatPropertyCompat<in T>>,
595 private var updateListeners: List<UpdateListener<T>>,
596 private var endListeners: List<EndListener<T>>,
597 private var endActions: List<EndAction>
598 ) {
599
600 /** The number of properties whose animations haven't ended. */
601 private var numPropertiesAnimating = properties.size
602
603 /**
604 * Update values that haven't yet been dispatched because not all property animations have
605 * updated yet.
606 */
607 private val undispatchedUpdates =
608 ArrayMap<FloatPropertyCompat<in T>, AnimationUpdate>()
609
610 /** Called when a DynamicAnimation updates. */
611 internal fun onInternalAnimationUpdate(
612 property: FloatPropertyCompat<in T>,
613 value: Float,
614 velocity: Float
615 ) {
616
617 // If this property animation isn't relevant to this listener, ignore it.
618 if (!properties.contains(property)) {
619 return
620 }
621
622 undispatchedUpdates[property] = AnimationUpdate(value, velocity)
623 maybeDispatchUpdates()
624 }
625
626 /**
627 * Called when a DynamicAnimation ends.
628 *
629 * @return True if this listener should be removed from the list of internal listeners, so
630 * it no longer receives updates from DynamicAnimations.
631 */
632 internal fun onInternalAnimationEnd(
633 property: FloatPropertyCompat<in T>,
634 canceled: Boolean,
635 finalValue: Float,
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500636 finalVelocity: Float,
637 isFling: Boolean
Joshua Tsuji408b9592019-11-07 18:32:58 -0500638 ): Boolean {
639
640 // If this property animation isn't relevant to this listener, ignore it.
641 if (!properties.contains(property)) {
642 return false
643 }
644
645 // Dispatch updates if we have one for each property.
646 numPropertiesAnimating--
647 maybeDispatchUpdates()
648
649 // If we didn't have an update for each property, dispatch the update for the ending
650 // property. This guarantees that an update isn't sent for this property *after* we call
651 // onAnimationEnd for that property.
652 if (undispatchedUpdates.contains(property)) {
653 updateListeners.forEach { updateListener ->
654 updateListener.onAnimationUpdateForProperty(
655 target,
656 UpdateMap<T>().also { it[property] = undispatchedUpdates[property] })
657 }
658
659 undispatchedUpdates.remove(property)
660 }
661
662 val allEnded = !arePropertiesAnimating(properties)
663 endListeners.forEach {
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500664 it.onAnimationEnd(
665 target, property, isFling, canceled, finalValue, finalVelocity,
666 allEnded)
667
668 // Check that the end listener didn't restart this property's animation.
669 if (isPropertyAnimating(property)) {
670 return false
671 }
672 }
Joshua Tsuji408b9592019-11-07 18:32:58 -0500673
674 // If all of the animations that this listener cares about have ended, run the end
675 // actions unless the animation was canceled.
676 if (allEnded && !canceled) {
677 endActions.forEach { it() }
678 }
679
680 return allEnded
681 }
682
683 /**
684 * Dispatch undispatched values if we've received an update from each of the animating
685 * properties.
686 */
687 private fun maybeDispatchUpdates() {
688 if (undispatchedUpdates.size >= numPropertiesAnimating &&
689 undispatchedUpdates.size > 0) {
690 updateListeners.forEach {
691 it.onAnimationUpdateForProperty(target, ArrayMap(undispatchedUpdates))
692 }
693
694 undispatchedUpdates.clear()
695 }
696 }
697 }
698
699 /** Return true if any animations are running on the object. */
700 fun isRunning(): Boolean {
701 return arePropertiesAnimating(springAnimations.keys.union(flingAnimations.keys))
702 }
703
704 /** Returns whether the given property is animating. */
705 fun isPropertyAnimating(property: FloatPropertyCompat<in T>): Boolean {
Joshua Tsujibdb6b202020-01-08 14:29:02 -0500706 return springAnimations[property]?.isRunning ?: false ||
707 flingAnimations[property]?.isRunning ?: false
Joshua Tsuji408b9592019-11-07 18:32:58 -0500708 }
709
710 /** Returns whether any of the given properties are animating. */
711 fun arePropertiesAnimating(properties: Set<FloatPropertyCompat<in T>>): Boolean {
712 return properties.any { isPropertyAnimating(it) }
713 }
714
715 /** Return the set of properties that will begin animating upon calling [start]. */
716 internal fun getAnimatedProperties(): Set<FloatPropertyCompat<in T>> {
717 return springConfigs.keys.union(flingConfigs.keys)
718 }
719
Joshua Tsuji959ac762020-02-13 03:21:56 -0500720 /**
721 * Cancels the given properties. This is typically called immediately by [cancel], unless this
722 * animator is under test.
723 */
724 internal fun cancelInternal(properties: Set<FloatPropertyCompat<in T>>) {
725 for (property in properties) {
726 flingAnimations[property]?.cancel()
727 springAnimations[property]?.cancel()
728 }
729 }
730
Joshua Tsuji408b9592019-11-07 18:32:58 -0500731 /** Cancels all in progress animations on all properties. */
732 fun cancel() {
Joshua Tsuji959ac762020-02-13 03:21:56 -0500733 cancelAction(flingAnimations.keys)
734 cancelAction(springAnimations.keys)
735 }
736
737 /** Cancels in progress animations on the provided properties only. */
738 fun cancel(vararg properties: FloatPropertyCompat<in T>) {
739 cancelAction(properties.toSet())
Joshua Tsuji408b9592019-11-07 18:32:58 -0500740 }
741
742 /**
743 * Container object for spring animation configuration settings. This allows you to store
744 * default stiffness and damping ratio values in a single configuration object, which you can
745 * pass to [spring].
746 */
747 data class SpringConfig internal constructor(
748 internal var stiffness: Float,
749 internal var dampingRatio: Float,
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500750 internal var startVelocity: Float = 0f,
751 internal var finalPosition: Float = UNSET
Joshua Tsuji408b9592019-11-07 18:32:58 -0500752 ) {
753
754 constructor() :
755 this(defaultSpring.stiffness, defaultSpring.dampingRatio)
756
757 constructor(stiffness: Float, dampingRatio: Float) :
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500758 this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f)
Joshua Tsuji408b9592019-11-07 18:32:58 -0500759
760 /** Apply these configuration settings to the given SpringAnimation. */
761 internal fun applyToAnimation(anim: SpringAnimation) {
762 val springForce = anim.spring ?: SpringForce()
763 anim.spring = springForce.apply {
764 stiffness = this@SpringConfig.stiffness
765 dampingRatio = this@SpringConfig.dampingRatio
766 finalPosition = this@SpringConfig.finalPosition
767 }
768
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500769 if (startVelocity != 0f) anim.setStartVelocity(startVelocity)
Joshua Tsuji408b9592019-11-07 18:32:58 -0500770 }
771 }
772
773 /**
774 * Container object for fling animation configuration settings. This allows you to store default
775 * friction values (as well as optional min/max values) in a single configuration object, which
776 * you can pass to [fling] and related methods.
777 */
778 data class FlingConfig internal constructor(
779 internal var friction: Float,
780 internal var min: Float,
781 internal var max: Float,
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500782 internal var startVelocity: Float
Joshua Tsuji408b9592019-11-07 18:32:58 -0500783 ) {
784
785 constructor() : this(defaultFling.friction)
786
787 constructor(friction: Float) :
788 this(friction, defaultFling.min, defaultFling.max)
789
790 constructor(friction: Float, min: Float, max: Float) :
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500791 this(friction, min, max, startVelocity = 0f)
Joshua Tsuji408b9592019-11-07 18:32:58 -0500792
793 /** Apply these configuration settings to the given FlingAnimation. */
794 internal fun applyToAnimation(anim: FlingAnimation) {
795 anim.apply {
796 friction = this@FlingConfig.friction
797 setMinValue(min)
798 setMaxValue(max)
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500799 setStartVelocity(startVelocity)
Joshua Tsuji408b9592019-11-07 18:32:58 -0500800 }
801 }
802 }
803
804 /**
805 * Listener for receiving values from in progress animations. Used with
806 * [PhysicsAnimator.addUpdateListener].
807 *
808 * @param <T> The type of the object being animated.
809 </T> */
810 interface UpdateListener<T> {
811
812 /**
813 * Called on each animation frame with the target object, and a map of FloatPropertyCompat
814 * -> AnimationUpdate, containing the latest value and velocity for that property. When
815 * multiple properties are animating together, the map will typically contain one entry for
816 * each property. However, you should never assume that this is the case - when a property
817 * animation ends earlier than the others, you'll receive an UpdateMap containing only that
818 * property's final update. Subsequently, you'll only receive updates for the properties
819 * that are still animating.
820 *
821 * Always check that the map contains an update for the property you're interested in before
822 * accessing it.
823 *
824 * @param target The animated object itself.
825 * @param values Map of property to AnimationUpdate, which contains that property
826 * animation's latest value and velocity. You should never assume that a particular property
827 * is present in this map.
828 */
829 fun onAnimationUpdateForProperty(
830 target: T,
831 values: UpdateMap<T>
832 )
833 }
834
835 /**
836 * Listener for receiving callbacks when animations end.
837 *
838 * @param <T> The type of the object being animated.
839 </T> */
840 interface EndListener<T> {
841
842 /**
843 * Called with the final animation values as each property animation ends. This can be used
844 * to respond to specific property animations concluding (such as hiding a view when ALPHA
845 * ends, even if the corresponding TRANSLATION animations have not ended).
846 *
847 * If you just want to run an action when all of the property animations have ended, you can
848 * use [PhysicsAnimator.withEndActions].
849 *
850 * @param target The animated object itself.
851 * @param property The property whose animation has just ended.
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500852 * @param wasFling Whether this property ended after a fling animation (as opposed to a
853 * spring animation). If this property was animated via [flingThenSpring], this will be true
854 * if the fling animation did not reach the min/max bounds, decelerating to a stop
855 * naturally. It will be false if it hit the bounds and was sprung back.
Joshua Tsuji408b9592019-11-07 18:32:58 -0500856 * @param canceled Whether the animation was explicitly canceled before it naturally ended.
857 * @param finalValue The final value of the animated property.
858 * @param finalVelocity The final velocity (in pixels per second) of the ended animation.
859 * This is typically zero, unless this was a fling animation which ended abruptly due to
860 * reaching its configured min/max values.
861 * @param allRelevantPropertyAnimsEnded Whether all properties relevant to this end listener
862 * have ended. Relevant properties are those which were animated alongside the
863 * [addEndListener] call where this animator was passed in. For example:
864 *
865 * animator
866 * .spring(TRANSLATION_X, 100f)
867 * .spring(TRANSLATION_Y, 200f)
868 * .withEndListener(firstEndListener)
869 * .start()
870 *
871 * firstEndListener will be called first for TRANSLATION_X, with allEnded = false,
872 * because TRANSLATION_Y is still running. When TRANSLATION_Y ends, it'll be called with
873 * allEnded = true.
874 *
875 * If a subsequent call to start() is made with other properties, those properties are not
876 * considered relevant and allEnded will still equal true when only TRANSLATION_X and
877 * TRANSLATION_Y end. For example, if immediately after the prior example, while
878 * TRANSLATION_X and TRANSLATION_Y are still animating, we called:
879 *
880 * animator.
881 * .spring(SCALE_X, 2f, stiffness = 10f) // That will take awhile...
882 * .withEndListener(secondEndListener)
883 * .start()
884 *
885 * firstEndListener will still be called with allEnded = true when TRANSLATION_X/Y end, even
886 * though SCALE_X is still animating. Similarly, secondEndListener will be called with
887 * allEnded = true as soon as SCALE_X ends, even if the translation animations are still
888 * running.
889 */
890 fun onAnimationEnd(
891 target: T,
892 property: FloatPropertyCompat<in T>,
Joshua Tsujiaaace7f2019-12-16 17:03:08 -0500893 wasFling: Boolean,
Joshua Tsuji408b9592019-11-07 18:32:58 -0500894 canceled: Boolean,
895 finalValue: Float,
896 finalVelocity: Float,
897 allRelevantPropertyAnimsEnded: Boolean
898 )
899 }
900
901 companion object {
902
903 /**
904 * Constructor to use to for new physics animator instances in [getInstance]. This is
905 * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that
906 * all code using the physics animator is given testable instances instead.
907 */
908 internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator
909
910 @JvmStatic
Joshua Tsuji569b5dc2019-12-19 14:14:58 -0500911 @Suppress("UNCHECKED_CAST")
Joshua Tsuji408b9592019-11-07 18:32:58 -0500912 fun <T : Any> getInstance(target: T): PhysicsAnimator<T> {
913 if (!animators.containsKey(target)) {
914 animators[target] = instanceConstructor(target)
915 }
916
917 return animators[target] as PhysicsAnimator<T>
918 }
919
920 /**
921 * Set whether all physics animators should log a lot of information about animations.
922 * Useful for debugging!
923 */
924 @JvmStatic
925 fun setVerboseLogging(debug: Boolean) {
926 verboseLogging = debug
927 }
928
Joshua Tsuji5b139c32020-01-30 18:03:09 -0500929 /**
930 * Estimates the end value of a fling that starts at the given value using the provided
931 * start velocity and fling configuration.
932 *
933 * This is only an estimate. Fling animations use a timing-based physics simulation that is
934 * non-deterministic, so this exact value may not be reached.
935 */
936 @JvmStatic
937 fun estimateFlingEndValue(
938 startValue: Float,
939 startVelocity: Float,
940 flingConfig: FlingConfig
941 ): Float {
942 val distance = startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
943 return Math.min(flingConfig.max, Math.max(flingConfig.min, startValue + distance))
944 }
945
Joshua Tsuji408b9592019-11-07 18:32:58 -0500946 @JvmStatic
947 fun getReadablePropertyName(property: FloatPropertyCompat<*>): String {
948 return when (property) {
949 DynamicAnimation.TRANSLATION_X -> "translationX"
950 DynamicAnimation.TRANSLATION_Y -> "translationY"
951 DynamicAnimation.TRANSLATION_Z -> "translationZ"
952 DynamicAnimation.SCALE_X -> "scaleX"
953 DynamicAnimation.SCALE_Y -> "scaleY"
954 DynamicAnimation.ROTATION -> "rotation"
955 DynamicAnimation.ROTATION_X -> "rotationX"
956 DynamicAnimation.ROTATION_Y -> "rotationY"
957 DynamicAnimation.SCROLL_X -> "scrollX"
958 DynamicAnimation.SCROLL_Y -> "scrollY"
959 DynamicAnimation.ALPHA -> "alpha"
960 else -> "Custom FloatPropertyCompat instance"
961 }
962 }
963 }
964}