| /* |
| * Copyright 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 androidx.animation |
| |
| import android.util.Log |
| import androidx.animation.AnimationEndReason.BoundReached |
| import androidx.animation.AnimationEndReason.Interrupted |
| import androidx.animation.AnimationEndReason.TargetReached |
| |
| /** |
| * This is the base class for [AnimatedValue]. It contains all the functionality of AnimatedValue. |
| * It is intended to be used as a base class for the other classes (such as [AnimatedFloat] to build |
| * on top of. |
| * |
| * Animations in this class allow and anticipate the animation target to change frequently. When |
| * the target changes as the animation is in-flight, the animation is expected to make a continuous |
| * transition to the new target. |
| * |
| * @param typeConverter A two way type converter that converts from value to [AnimationVector1D], |
| * [AnimationVector2D], [AnimationVector3D], or [AnimationVector4D], and vice |
| * versa. |
| * @param clock An animation clock observable controlling the progression of the animated value |
| * @param visibilityThreshold Visibility threshold of the animation specifies the end condition: |
| * for t > duration, value < visibilityThreshold. Null value defaults to [PhysicsBuilder] |
| * default. |
| */ |
| sealed class BaseAnimatedValue<T, V : AnimationVector>( |
| internal val typeConverter: TwoWayConverter<T, V>, |
| private val clock: AnimationClockObservable, |
| internal val visibilityThreshold: V? |
| ) { |
| |
| /** |
| * Current value of the animation. |
| */ |
| abstract var value: T |
| protected set |
| |
| /** |
| * Indicates whether the animation is running. |
| */ |
| var isRunning: Boolean = false |
| internal set |
| |
| /** |
| * The target of the current animation. This target will not be the same as the value of the |
| * animation, until the animation finishes un-interrupted. |
| */ |
| var targetValue: T |
| get() { |
| if (_targetBackingField != null) { |
| return _targetBackingField!! |
| } else { |
| return value |
| } |
| } |
| internal set(newTarget) { |
| _targetBackingField = newTarget |
| } |
| |
| // TODO: remove the backing field when b/148422703 is fixed |
| private var _targetBackingField: T? = null |
| get() { |
| if (field == null) { |
| field = value |
| } |
| return field |
| } |
| |
| /** |
| * Velocity of the animation. The velocity will be of [AnimationVector1D], [AnimationVector2D], |
| * [AnimationVector3D], or [AnimationVector4D] type. |
| */ |
| internal var velocityVector: V |
| get() = _velocityBackField!! |
| set(value) { |
| _velocityBackField = value |
| } |
| |
| // TODO: remove the backing field when b/148422703 is fixed |
| private var _velocityBackField: V? = null |
| get() { |
| if (field == null) { |
| field = typeConverter.convertToVector(value).newInstance() |
| } |
| return field |
| } |
| |
| internal var onEnd: ((AnimationEndReason, T) -> Unit)? = null |
| private lateinit var anim: AnimationWrapper<T, V> |
| private var startTime: Long = Unset |
| // last frame time only gets updated during the animation pulse. It will be reset at the |
| // end of the animation. |
| private var lastFrameTime: Long = Unset |
| |
| private var animationClockObserver = object : AnimationClockObserver { |
| override fun onAnimationFrame(frameTimeMillis: Long) { |
| doAnimationFrame(frameTimeMillis) |
| } |
| } |
| |
| private fun defaultPhysicsBuilder(): PhysicsBuilder<T> { |
| return visibilityThreshold?.let { |
| PhysicsBuilder(displacementThreshold = typeConverter.convertFromVector(it)) |
| } ?: PhysicsBuilder() |
| } |
| |
| // TODO: Need a test for animateTo(...) being called with the same target value |
| /** |
| * Sets the target value, which effectively starts an animation to change the value from [value] |
| * to the target value. If there is already an animation in flight, this method will interrupt |
| * the ongoing animation, invoke [onEnd] that is associated with that animation, and start |
| * a new animation from the current value to the new target value. |
| * |
| * @param targetValue The new value to animate to |
| * @param anim The animation that will be used to animate from the current value to the new |
| * target value. If unspecified, a spring animation will be used by default. |
| * @param onEnd An optional callback that will be invoked when the animation finished by any |
| * reason. |
| */ |
| fun animateTo( |
| targetValue: T, |
| anim: AnimationBuilder<T> = defaultPhysicsBuilder(), |
| onEnd: ((AnimationEndReason, T) -> Unit)? = null |
| ) { |
| if (isRunning) { |
| notifyEnded(Interrupted, value) |
| } |
| |
| this.targetValue = targetValue |
| val animationWrapper = TargetBasedAnimationWrapper( |
| value, velocityVector, targetValue, anim.build(typeConverter), typeConverter |
| ) |
| |
| if (DEBUG) { |
| Log.w( |
| "AnimValue", "To value called: start value: $value," + |
| "end value: $targetValue, velocity: $velocityVector" |
| ) |
| } |
| this.onEnd = onEnd |
| startAnimation(animationWrapper) |
| } |
| |
| /** |
| * Sets the current value to the target value immediately, without any animation. |
| * |
| * @param targetValue The new target value to set [value] to. |
| */ |
| open fun snapTo(targetValue: T) { |
| stop() |
| value = targetValue |
| this.targetValue = targetValue |
| } |
| |
| /** |
| * Stops any on-going animation. No op if no animation is running. Note that this method does |
| * not skip the animation value to its target value. Rather the animation will be stopped in its |
| * track. |
| */ |
| fun stop() { |
| if (isRunning) { |
| endAnimation(Interrupted) |
| } |
| } |
| |
| internal fun notifyEnded(endReason: AnimationEndReason, endValue: T) { |
| val onEnd = this.onEnd |
| this.onEnd = null |
| onEnd?.invoke(endReason, endValue) |
| } |
| |
| private fun doAnimationFrame(timeMillis: Long) { |
| val playtime: Long |
| if (startTime == Unset) { |
| startTime = timeMillis |
| playtime = 0 |
| } else { |
| playtime = timeMillis - startTime |
| } |
| |
| lastFrameTime = timeMillis |
| value = anim.getValue(playtime) |
| velocityVector = anim.getVelocity(playtime) |
| |
| checkFinished(playtime) |
| } |
| |
| protected open fun checkFinished(playtime: Long) { |
| val animationFinished = anim.isFinished(playtime) |
| if (DEBUG) { |
| val debugLogMessage = if (animationFinished) |
| "value = $value, playtime = $playtime, animation finished" |
| else |
| "value = $value, playtime = $playtime, velocity: $velocityVector" |
| Log.w("AnimValue", debugLogMessage) |
| } |
| if (animationFinished) endAnimation() |
| } |
| |
| internal fun startAnimation(anim: AnimationWrapper<T, V>) { |
| this.anim = anim |
| // Quick sanity check before officially starting |
| if (anim.isFinished(0)) { |
| // If the animation value & velocity is already meeting the finished condition before |
| // the animation even starts, end it now. |
| endAnimation() |
| return |
| } |
| |
| if (isRunning) { |
| startTime = lastFrameTime |
| } else { |
| startTime = Unset |
| isRunning = true |
| clock.subscribe(animationClockObserver) |
| } |
| if (DEBUG) { |
| Log.w("AnimValue", "start animation") |
| } |
| } |
| |
| internal fun endAnimation(endReason: AnimationEndReason = TargetReached) { |
| clock.unsubscribe(animationClockObserver) |
| isRunning = false |
| startTime = Unset |
| lastFrameTime = Unset |
| if (DEBUG) { |
| Log.w("AnimValue", "end animation with reason $endReason") |
| } |
| notifyEnded(endReason, value) |
| // reset velocity after notifyFinish as we might need to return it in onFinished callback |
| // depending on whether or not velocity was involved in the animation |
| velocityVector.reset() |
| } |
| } |
| |
| /** |
| * AnimatedValue is an animatable value holder. It can hold any type of value, and automatically |
| * animate the value change when the value is changed via [animateTo]. AnimatedValue supports value |
| * change during an ongoing value change animation. When that happens, a new animation will |
| * transition AnimatedValue from its current value (i.e. value at the point of interruption) to the |
| * new target. This ensures that the value change is always continuous. |
| * |
| */ |
| abstract class AnimatedValue<T, V : AnimationVector>( |
| typeConverter: TwoWayConverter<T, V>, |
| clock: AnimationClockObservable, |
| visibilityThreshold: V? = null |
| ) : BaseAnimatedValue<T, V>(typeConverter, clock, visibilityThreshold) { |
| val velocity: V |
| get() = velocityVector |
| } |
| |
| /** |
| * This class inherits most of the functionality from BaseAnimatedValue. In addition, it tracks |
| * velocity and supports the definition of bounds. Once bounds are defined using [setBounds], the |
| * animation will consider itself finished when it reaches the upper or lower bound, even when the |
| * velocity is non-zero. |
| * |
| * @param clock An animation clock observable controlling the progression of the animated value |
| */ |
| abstract class AnimatedFloat( |
| clock: AnimationClockObservable, |
| visibilityThreshold: Float = Spring.DefaultDisplacementThreshold |
| ) : BaseAnimatedValue<Float, AnimationVector1D>( |
| FloatToVectorConverter, clock, AnimationVector(visibilityThreshold)) { |
| |
| /** |
| * Lower bound of the animation value. When animations reach this lower bound, it will |
| * automatically stop with [AnimationEndReason] being [AnimationEndReason.BoundReached]. |
| * This bound is [Float.NEGATIVE_INFINITY] by default. It can be adjusted via [setBounds]. |
| */ |
| var min: Float = Float.NEGATIVE_INFINITY |
| private set |
| /** |
| * Upper bound of the animation value. When animations reach this upper bound, it will |
| * automatically stop with [AnimationEndReason] being [AnimationEndReason.BoundReached]. |
| * This bound is [Float.POSITIVE_INFINITY] by default. It can be adjusted via [setBounds]. |
| */ |
| var max: Float = Float.POSITIVE_INFINITY |
| private set |
| |
| val velocity: Float |
| get() = velocityVector.value |
| |
| /** |
| * Sets up the bounds that the animation should be constrained to. Note that when the animation |
| * reaches the bounds it will stop right away, even when there is remaining velocity. |
| * |
| * @param min Lower bound of the animation value. Defaults to [Float.NEGATIVE_INFINITY] |
| * @param max Upper bound of the animation value. Defaults to [Float.POSITIVE_INFINITY] |
| */ |
| fun setBounds(min: Float = Float.NEGATIVE_INFINITY, max: Float = Float.POSITIVE_INFINITY) { |
| if (max < min) { |
| // throw exception? |
| } |
| this.min = min |
| this.max = max |
| } |
| |
| override fun snapTo(targetValue: Float) { |
| super.snapTo(targetValue.coerceIn(min, max)) |
| } |
| |
| override fun checkFinished(playtime: Long) { |
| if (value < min) { |
| value = min |
| endAnimation(BoundReached) |
| } else if (value > max) { |
| value = max |
| endAnimation(BoundReached) |
| } else { |
| super.checkFinished(playtime) |
| } |
| } |
| } |
| |
| /** |
| * Typealias for lambda that will be invoked when fling animation ends. |
| * Unlike [AnimatedValue.animateTo] onEnd, this lambda includes 3rd param remainingVelocity, |
| * that represents velocity that wasn't consumed after fling finishes. |
| */ |
| // TODO: Consolidate onAnimationEnd and onEnd |
| typealias OnAnimationEnd = |
| (endReason: AnimationEndReason, endValue: Float, remainingVelocity: Float) -> Unit |
| |
| /** |
| * Starts a fling animation with the specified starting velocity. |
| * |
| * @param startVelocity Starting velocity of the fling animation |
| * @param decay The decay animation used for slowing down the animation from the starting |
| * velocity |
| * @param onEnd An optional callback that will be invoked when this fling animation is |
| * finished. |
| */ |
| // TODO: Figure out an API for customizing the type of decay & the friction |
| fun AnimatedFloat.fling( |
| startVelocity: Float, |
| decay: DecayAnimation = ExponentialDecay(), |
| onEnd: OnAnimationEnd? = null |
| ) { |
| if (isRunning) { |
| notifyEnded(Interrupted, value) |
| } |
| |
| this.onEnd = { endReason, endValue -> |
| onEnd?.invoke(endReason, endValue, velocity) |
| } |
| |
| // start from current value with the given velocity |
| targetValue = decay.getTarget(value, startVelocity) |
| val animWrapper = DecayAnimationWrapper(value, startVelocity, decay) |
| startAnimation(animWrapper) |
| } |
| |
| // TODO: Devs may want to change the target animation based on how close the target is to the |
| // snapping position. |
| /** |
| * Starts a fling animation with the specified starting velocity. |
| * |
| * @param startVelocity Starting velocity of the fling animation |
| * @param adjustTarget A lambda that takes in the projected destination based on the decay |
| * animation, and returns a nullable TargetAnimation object that contains a |
| * new destination and an animation to animate to the new destination. This |
| * lambda should return null when the original target is respected. |
| * @param decay The decay animation used for slowing down the animation from the starting |
| * velocity |
| * @param onEnd An optional callback that will be invoked when the animation |
| * finished by any reason. |
| */ |
| fun AnimatedFloat.fling( |
| startVelocity: Float, |
| decay: DecayAnimation = ExponentialDecay(), |
| adjustTarget: (Float) -> TargetAnimation?, |
| onEnd: OnAnimationEnd? = null |
| ) { |
| if (isRunning) { |
| notifyEnded(Interrupted, value) |
| } |
| |
| this.onEnd = { endReason, endValue -> |
| onEnd?.invoke(endReason, endValue, velocity) |
| } |
| |
| // start from current value with the given velocity |
| if (DEBUG) { |
| Log.w("AnimFloat", "Calculating target. Value: $value, velocity: $startVelocity") |
| } |
| targetValue = decay.getTarget(value, startVelocity) |
| val targetAnimation = adjustTarget(targetValue) |
| if (DEBUG) { |
| Log.w( |
| "AnimFloat", "original targetValue: $targetValue, new target:" + |
| " ${targetAnimation?.target}" |
| ) |
| } |
| if (targetAnimation == null) { |
| val animWrapper = DecayAnimationWrapper(value, startVelocity, decay) |
| startAnimation(animWrapper) |
| } else { |
| targetValue = targetAnimation.target |
| val animWrapper = TargetBasedAnimationWrapper( |
| value, |
| AnimationVector1D(startVelocity), |
| targetAnimation.target, |
| targetAnimation.animation.build(typeConverter), |
| typeConverter |
| ) |
| startAnimation(animWrapper) |
| } |
| } |
| |
| private const val Unset: Long = -1 |
| |
| /** |
| * Factory method for creating an [AnimatedValue] object, and initialize the value field to |
| * [initVal]. |
| * |
| * @param initVal Initial value to initialize the animation to. |
| * @param typeConverter Converter for converting value type [T] to [AnimationVector], and vice versa |
| * @param clock The animation clock used to drive the animation. |
| */ |
| fun <T, V : AnimationVector> AnimatedValue( |
| initVal: T, |
| typeConverter: TwoWayConverter<T, V>, |
| clock: AnimationClockObservable, |
| visibilityThreshold: V = typeConverter |
| .convertToVector(initVal) |
| .newInstanceOfValue(Spring.DefaultDisplacementThreshold) |
| ): AnimatedValue<T, V> = |
| AnimatedValueImpl(initVal, typeConverter, clock, visibilityThreshold) |
| |
| /** |
| * Factory method for creating an [AnimatedVector] object, and initialize the value field to |
| * [initVal]. |
| * |
| * @param initVal Initial value to initialize the animation to. |
| * @param clock The animation clock used to drive the animation. |
| */ |
| fun <V : AnimationVector> AnimatedVector( |
| initVal: V, |
| clock: AnimationClockObservable, |
| visibilityThreshold: V = initVal.newInstanceOfValue(Spring.DefaultDisplacementThreshold) |
| ): AnimatedValue<V, V> = |
| AnimatedValueImpl(initVal, TwoWayConverter({ it }, { it }), clock, visibilityThreshold) |
| |
| /** |
| * Factory method for creating an [AnimatedFloat] object, and initialize the value field to |
| * [initVal]. |
| * |
| * @param initVal Initial value to initialize the animation to. |
| * @param clock The animation clock used to drive the animation. |
| * @param visibilityThreshold Threshold at which the animation may round off to its target value. |
| */ |
| fun AnimatedFloat( |
| initVal: Float, |
| clock: AnimationClockObservable, |
| visibilityThreshold: Float = Spring.DefaultDisplacementThreshold |
| ): AnimatedFloat = AnimatedFloatImpl(initVal, clock, visibilityThreshold) |
| |
| // Private impl for AnimatedValue |
| private class AnimatedValueImpl<T, V : AnimationVector>( |
| initVal: T, |
| typeConverter: TwoWayConverter<T, V>, |
| clock: AnimationClockObservable, |
| visibilityThreshold: V |
| ) : AnimatedValue<T, V>(typeConverter, clock, visibilityThreshold) { |
| override var value: T = initVal |
| } |
| |
| // Private impl for AnimatedFloat |
| private class AnimatedFloatImpl( |
| initVal: Float, |
| clock: AnimationClockObservable, |
| visibilityThreshold: Float |
| ) : AnimatedFloat(clock, visibilityThreshold) { |
| override var value: Float = initVal |
| } |
| |
| private fun <V : AnimationVector> V.newInstanceOfValue(value: Float): V { |
| return newInstance().apply { |
| (0 until size).forEach { set(it, value) } |
| } |
| } |