blob: ce0c1e3afbffc6d0f0770a3be47ccae881684ef9 [file] [log] [blame]
/*
* 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 androidx.ui.util.fastFirstOrNull
import kotlin.experimental.ExperimentalTypeInference
/**
* Static specification for the transition from one state to another.
*
* Each property involved in the states that the transition is from and to can have an animation
* associated with it. When such an animation is defined, the animation system will be using it
* instead of the default [SpringAnimation] animation to animate the value change for that property.
*
* @sample androidx.animation.samples.TransitionSpecWith3Properties
**/
class TransitionSpec<S> internal constructor(private val fromToPairs: Array<out Pair<S?, S?>>) {
/**
* Optional state where should we start switching after this transition finishing.
*/
var nextState: S? = null
/**
* The interruption handling mechanism. The default interruption handling is
* [InterruptionHandling.PHYSICS]. Meaning both value and velocity of the property will be
* preserved as the target state (and therefore target animation value) changes.
* [InterruptionHandling.TWEEN], which only ensures the continuity of current animation value.
* [InterruptionHandling.UNINTERRUPTIBLE] defines a scenario where an animation is so important
* that it cannot be interrupted, so the new state request has to be queued.
* [InterruptionHandling.SNAP_TO_END] can be used for cases where higher priority events (such
* as user gesture) come in and the on-going animation needs to finish immediately to give way
* to the user events.
*/
var interruptionHandling: InterruptionHandling = InterruptionHandling.PHYSICS
/**
* The default animation to use when it wasn't explicitly provided for a property
*/
internal var defaultAnimation: DefaultTransitionAnimation = SpringTransition()
private val propAnimation: MutableMap<PropKey<*, *>, Animation<*>> = mutableMapOf()
internal fun <T, V : AnimationVector> getAnimationForProp(prop: PropKey<T, V>): Animation<V> {
@Suppress("UNCHECKED_CAST")
return (propAnimation.getOrPut(prop,
{ defaultAnimation.createDefault(prop.typeConverter) })) as Animation<V>
}
internal fun defines(from: S?, to: S?) =
fromToPairs.any { it.first == from && it.second == to }
/**
* Associates a property with a [AnimationBuilder]
*
* @param builder: [AnimationBuilder] for animating [this] property value changes
*/
infix fun <T, V : AnimationVector> PropKey<T, V>.using(builder: AnimationBuilder<T>) {
propAnimation[this] = builder.build(typeConverter)
}
/**
* Creates a [Tween] animation, initialized with [init]
*
* @param init Initialization function for the [Tween] animation
*/
fun <T> tween(init: TweenBuilder<T>.() -> Unit): DurationBasedAnimationBuilder<T> =
TweenBuilder<T>().apply(init)
/**
* Creates a [SpringAnimation] animation, initialized with [init]
*
* @param init Initialization function for the [SpringAnimation] animation
*/
fun <T> physics(init: PhysicsBuilder<T>.() -> Unit): AnimationBuilder<T> =
PhysicsBuilder<T>().apply(init)
/**
* Creates a [Keyframes] animation, initialized with [init]
*
* @param init Initialization function for the [Keyframes] animation
*/
fun <T> keyframes(init: KeyframesBuilder<T>.() -> Unit): KeyframesBuilder<T> =
KeyframesBuilder<T>().apply(init)
/**
* Creates a [Repeatable] animation, initialized with [init]
*
* @param init Initialization function for the [Repeatable] animation
*/
fun <T> repeatable(init: RepeatableBuilder<T>.() -> Unit): AnimationBuilder<T> =
RepeatableBuilder<T>().apply(init)
/**
* Creates a Snap animation for immediately switching the animating value to the end value.
*/
fun <T> snap(): AnimationBuilder<T> = SnapBuilder()
}
internal interface DefaultTransitionAnimation {
fun <T, V : AnimationVector> createDefault(typeConverter: TwoWayConverter<T, V>): Animation<V>
}
internal class SnapTransition : DefaultTransitionAnimation {
override fun <T, V : AnimationVector> createDefault(
typeConverter: TwoWayConverter<T, V>
): Animation<V> {
return SnapBuilder<T>().build(typeConverter)
}
}
internal class SpringTransition : DefaultTransitionAnimation {
override fun <T, V : AnimationVector> createDefault(
typeConverter: TwoWayConverter<T, V>
): Animation<V> {
return PhysicsBuilder<T>().build(typeConverter)
}
}
/**
* [TransitionDefinition] contains all the animation related configurations that will be used in
* a state-based transition. It holds a set of [TransitionState]s and an optional set of
* [TransitionSpec]s. It can be used in [android.ui.animation.Transition] to create a state-based
* animation in Compose.
*
* Each [TransitionState] specifies how the UI should look in terms of values
* associated with properties that differentiates the UI from one conceptual state to anther. Each
* [TransitionState] can be considered as a snapshot of the UI in the form of property values.
*
* [TransitionSpec] defines how to animate from one state to another with a specific animation for
* each property defined in the states. [TransitionSpec] can be created using [transition] method
* inside of a [TransitionDefinition]. Currently the animations supported in a [transition] are:
* [TransitionSpec.tween], [TransitionSpec.keyframes], [TransitionSpec.physics],
* [TransitionSpec.snap], [TransitionSpec.repeatable]. When no [TransitionSpec] is specified,
* the default [TransitionSpec.physics] animation will be used for all properties involved.
* Similarly, when no animation is provided in a [TransitionSpec] for a particular property,
* the default physics animation will be used. For each [transition], both the from and the to state
* can be omitted. Omitting in this case is equivalent to a wildcard on the starting state or ending
* state. When both are omitted at the same time, it means this transition applies to all the state
* transitions unless a more specific transition have been defined.
*
* To create a [TransitionDefinition], there are generally 3 steps involved:
*
* __Step 1__: Create PropKeys. One [PropKey] is required for each property/value that needs to
* be animated. These should be file level properties, so they are visible to
* [TransitionDefinition] ( which will be created in step 3).
*
* val radius = FloatPropKey()
* val alpha = FloatPropKey()
*
* __Step 2__ (optional): Create state names.
*
* This is an optional but recommended step to create a reference for different states that the
* animation should end at. State names can be of type [T], which means they can be string,
* integer, etc, or any custom object, so long as they are consistent.
* It is recommended to either reuse the states that you already defined (e.g.
* TogglableState.On, TogglableState.Off, etc) for animating those state changes, or create
* an enum class for all the animation states.
*
* enum class ButtonState {
* Released, Pressed, Disabled
* }
*
* __Step 3__: Create a [TransitionDefinition] using the animation DSL.
*
* [TransitionDefinition] is conceptually an animation configuration that defines:
* 1) States, each of which are described as a set of values. Each value is associated with a
* PropKey.
* 2) Optional transitions, for how to animate from one state to another.
*
* @sample androidx.animation.samples.TransitionDefSample
*
* Once a [TransitionDefinition] is created, [androidx.ui.animation.Transition] composable can take
* it as an input and create a state-based transition in compose.
*
* @see [androidx.ui.animation.Transition]
*/
class TransitionDefinition<T> {
internal val states: MutableMap<T, StateImpl<T>> = mutableMapOf()
internal lateinit var defaultState: StateImpl<T>
private val transitionSpecs: MutableList<TransitionSpec<T>> = mutableListOf()
// TODO: Consider also having the initial defined at call site for cases where many components
// share the same transition def
// TODO: (Optimization) Type param in TransitionSpec requires this defaultTransitionSpec to be
// re-created at least for each state type T. Consider dropping this T beyond initial sanity
// check.
private val defaultTransitionSpec = TransitionSpec<T>(arrayOf(null to null))
/**
* Defines all the properties and their values associated with the state with the name: [name]
* The first state defined in the transition definition will be the default state, whose
* property values will be used as its initial values to createAnimation from.
*
* Note that the first [MutableTransitionState] created with [state] in a [TransitionDefinition]
* will be used as the initial state.
*
* @param name The name of the state, which can be used to createAnimation from or to this state
* @param init Lambda to initialize a state
*/
fun state(name: T, init: MutableTransitionState.() -> Unit) {
val newState = StateImpl(name).apply(init)
states[name] = newState
if (!::defaultState.isInitialized) {
defaultState = newState
}
}
/**
* Defines a transition from state [fromState] to [toState]. When animating from one state to
* another, [TransitionAnimation] will find the most specific matching transition, and use the
* animations defined in it for the state transition. Both [fromState] and [toState] are
* optional. When undefined, it means a wildcard transition going from/to any state.
*
* @param fromState The state that the transition will be animated from
* @param toState The state that the transition will be animated to
* @param init Lambda to initialize the transition
*/
fun transition(fromState: T? = null, toState: T? = null, init: TransitionSpec<T>.() -> Unit) {
transition(fromState to toState, init = init)
}
/**
* Defines a transition from state first value to the second value of the [fromToPairs].
* When animating from one state to another, [TransitionAnimation] will find the most specific
* matching transition, and use the animations defined in it for the state transition. Both
* values in the pair can be null. When they are null, it means a wildcard transition going
* from/to any state.
*
* Sample of usage with [Pair]s infix extension [to]:
* @sample androidx.animation.samples.TransitionSpecWithPairs
*
* @param fromToPairs The pairs of from and to states for this transition
* @param init Lambda to initialize the transition
*/
fun transition(vararg fromToPairs: Pair<T?, T?>, init: TransitionSpec<T>.() -> Unit) {
val newSpec = TransitionSpec(fromToPairs).apply(init)
transitionSpecs.add(newSpec)
}
/**
* With this transition definition we are saying that every time we reach the
* state 'from' we should immediately snap to 'to' state instead.
*
* Sample of usage with [Pair]s infix extension [to]:
* snapTransition(State.Released to State.Pressed)
*
* @param fromToPairs The pairs of states for this transition
* @param nextState Optional state where should we start switching after snap
*/
fun snapTransition(vararg fromToPairs: Pair<T?, T?>, nextState: T? = null) =
transition(*fromToPairs) {
this.nextState = nextState
defaultAnimation = SnapTransition()
}
internal fun getSpec(fromState: T, toState: T): TransitionSpec<T> {
return transitionSpecs.fastFirstOrNull { it.defines(fromState, toState) }
?: transitionSpecs.fastFirstOrNull { it.defines(fromState, null) }
?: transitionSpecs.fastFirstOrNull { it.defines(null, toState) }
?: transitionSpecs.fastFirstOrNull { it.defines(null, null) }
?: defaultTransitionSpec
}
/**
* Returns a state holder for the specific state [name]. Useful for the cases
* where we don't need actual animation to be happening like in tests.
*/
fun getStateFor(name: T): TransitionState = states.getValue(name)
}
/**
* Creates a transition animation using the transition definition and the given clock.
*
* @param clock The clock source for animation to get frame time from.
*/
fun <T> TransitionDefinition<T>.createAnimation(
clock: AnimationClockObservable,
initState: T? = null
) = TransitionAnimation(this, clock, initState)
/**
* Creates a [TransitionDefinition] using the [init] function to initialize it.
*
* @param init Initialization function for the [TransitionDefinition]
*/
@OptIn(ExperimentalTypeInference::class)
fun <T> transitionDefinition(@BuilderInference init: TransitionDefinition<T>.() -> Unit) =
TransitionDefinition<T>().apply(init)
enum class InterruptionHandling {
PHYSICS,
SNAP_TO_END, // Not yet supported
TWEEN, // Not yet supported
UNINTERRUPTIBLE
}