| /* |
| * 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.InterruptionHandling.UNINTERRUPTIBLE |
| |
| /** |
| * [TransitionAnimation] is responsible for animating from one set of property values (i.e. one |
| * [TransitionState]) to another. More specifically, it reads the property values out of the new |
| * state that it is going to, as well as the animations defined for the properties, and run these |
| * animations until all properties have reached their pre-defined values in the new state. When no |
| * animation is specified for a property, a default [SpringAnimation] animation will be used. |
| * |
| * [TransitionAnimation] may be interrupted while the animation is on-going by a request to go |
| * to another state. [TransitionAnimation] ensures that all the animating properties preserve their |
| * current value and velocity as they createAnimation to the new state. |
| * |
| * Once a [TransitionDefinition] is instantiated, a [TransitionAnimation] can be created via |
| * [TransitionDefinition.createAnimation]. |
| */ |
| class TransitionAnimation<T> ( |
| private val def: TransitionDefinition<T>, |
| private val clock: AnimationClockObservable, |
| initState: T? = null |
| ) : TransitionState { |
| |
| var onUpdate: (() -> Unit)? = null |
| var onStateChangeFinished: ((T) -> Unit)? = null |
| var isRunning = false |
| private set |
| |
| private val UNSET = -1L |
| private var fromState: StateImpl<T> |
| private var toState: StateImpl<T> |
| private val currentState: AnimationState<T> |
| private var startTime: Long = UNSET |
| private var lastFrameTime: Long = UNSET |
| private var pendingState: StateImpl<T>? = null |
| private var currentAnimations: MutableMap<PropKey<Any>, Animation<Any>> = mutableMapOf() |
| private var startVelocityMap: MutableMap<PropKey<Any>, Float> = mutableMapOf() |
| private val animationClockObserver = object : AnimationClockObserver { |
| override fun onAnimationFrame(frameTimeMillis: Long) { |
| doAnimationFrame(frameTimeMillis) |
| } |
| } |
| |
| // TODO("Create a more efficient code path for default only transition def") |
| |
| init { |
| // If an initial state is specified in the ctor, use that instead of the default state. |
| val defaultState: StateImpl<T> |
| if (initState == null) { |
| defaultState = def.defaultState |
| } else { |
| defaultState = def.states[initState]!! |
| } |
| currentState = AnimationState(defaultState, defaultState.name) |
| // Need to come up with a better plan to avoid the foot gun of accidentally modifying state |
| fromState = defaultState |
| toState = defaultState |
| } |
| |
| // Interpolate current state and the new state |
| private fun setState(newState: StateImpl<T>) { |
| if (isRunning) { |
| val currentSpec = def.getSpec(fromState.name, toState.name) |
| if (currentSpec.interruptionHandling == UNINTERRUPTIBLE) { |
| pendingState = newState |
| return |
| } |
| } |
| |
| val transitionSpec = def.getSpec(toState.name, newState.name) |
| val playTime = getPlayTime() |
| // TODO: handle the states that have only partial properties defined |
| // For now assume all the properties are defined in all states |
| |
| // TODO: Support different interruption types |
| // For now assume continuing with the same value, and for floats the same velocity |
| for ((prop, _) in newState.props) { |
| val startVelocity = startVelocityMap[prop] ?: 0f |
| val currentVelocity = currentAnimations[prop]?.getVelocity( |
| playTime, fromState[prop], toState[prop], startVelocity, |
| prop::interpolate |
| ) ?: 0f |
| startVelocityMap[prop] = currentVelocity |
| currentAnimations[prop] = transitionSpec.getAnimationForProp(prop) |
| // TODO: Will need to track a few timelines if we support partially defined list of |
| // props in each state. |
| } |
| |
| fromState = AnimationState(currentState, toState.name) |
| toState = newState |
| if (DEBUG) { |
| Log.w("TransAnim", "Animating to new state: ${toState.name}") |
| } |
| |
| // Start animation should be called after all the setup has been done |
| startAnimation() |
| } |
| |
| private fun getPlayTime(): Long { |
| if (startTime == UNSET) { |
| return 0L |
| } |
| return lastFrameTime - startTime |
| } |
| |
| /** |
| * Starts the animation to go to a new state with the given state name. |
| * |
| * @param name Name of the [TransitionState] that is defined in the [TransitionDefinition]. |
| */ |
| fun toState(name: T) { |
| val nextState = def.states[name] |
| if (nextState == null) { |
| // Throw exception or ignore? |
| } else if (pendingState != null && toState.name == name) { |
| // just canceling the pending state |
| pendingState = null |
| } else if ((pendingState ?: toState).name == name) { |
| // already targeting this state |
| } else { |
| setState(nextState) |
| } |
| } |
| |
| /** |
| * Gets the value of a property with a given property key. |
| * |
| * @param propKey Property key (defined in [TransitionDefinition]) for a specific property |
| */ |
| override operator fun <T> get(propKey: PropKey<T>): T { |
| return currentState[propKey] |
| } |
| |
| // Start animation if not running, otherwise reset start time |
| private fun startAnimation() { |
| if (!isRunning) { |
| isRunning = true |
| clock.subscribe(animationClockObserver) |
| } else { |
| startTime = lastFrameTime |
| } |
| } |
| |
| private fun doAnimationFrame(frameTimeMillis: Long) { |
| // Remove finished animations |
| lastFrameTime = frameTimeMillis |
| if (startTime == UNSET) { |
| startTime = frameTimeMillis |
| } |
| |
| val playTime = getPlayTime() |
| for ((prop, animation) in currentAnimations) { |
| val velocity: Float = startVelocityMap[prop] ?: 0f |
| currentState[prop] = |
| animation.getValue(playTime, fromState[prop], toState[prop], velocity, |
| prop::interpolate) |
| } |
| |
| // Prune the finished animations |
| currentAnimations.entries.removeAll { |
| val prop = it.key |
| val velocity: Float = startVelocityMap[prop] ?: 0f |
| it.value.isFinished(playTime, fromState[prop], toState[prop], velocity) |
| } |
| |
| onUpdate?.invoke() |
| |
| // call end animation when all animations end |
| if (currentAnimations.isEmpty()) { |
| // All animations have finished. Snap all values to end value |
| for (prop in toState.props.keys) { |
| currentState[prop] = toState[prop] |
| } |
| startVelocityMap.clear() |
| |
| endAnimation() |
| val currentStateName = toState.name |
| val spec = def.getSpec(fromState.name, toState.name) |
| val nextState = def.states[spec.nextState] |
| fromState = toState |
| |
| // Uninterruptible transition to the next state takes a priority over the pending state. |
| if (nextState != null && spec.interruptionHandling == UNINTERRUPTIBLE) { |
| setState(nextState) |
| } else if (pendingState != null) { |
| setState(pendingState!!) |
| pendingState = null |
| } else if (nextState != null) { |
| setState(nextState) |
| } |
| onStateChangeFinished?.invoke(currentStateName) |
| } |
| } |
| |
| private fun endAnimation() { |
| clock.unsubscribe(animationClockObserver) |
| startTime = UNSET |
| lastFrameTime = UNSET |
| isRunning = false |
| } |
| } |
| |
| /** |
| * Private class allows mutation on the prop values. |
| */ |
| private class AnimationState<T>(state: StateImpl<T>, name: T) : StateImpl<T>(name) { |
| |
| init { |
| for ((prop, value) in state.props) { |
| // Make a copy of the new values |
| val newValue = prop.interpolate(value, value, 0f) |
| props[prop] = newValue |
| } |
| } |
| |
| override operator fun <T> set(propKey: PropKey<T>, prop: T) { |
| @Suppress("UNCHECKED_CAST") |
| propKey as PropKey<Any> |
| props[propKey] = prop as Any |
| } |
| } |