Animatable
Compose uses an Animatable to implement animations, which can be understood as a Value holder that can be used as an animation property. When a Value it holds is updated through an animateTo, the process can automatically evolve as an animation. Unlike traditional view-based animations, which use coroutines internally to calculate the animation’s intermediate process, the trigger function animateTo() is suspend, which greatly guarantees animation performance at runtime. Basic usage:
@Composable
fun Demo(a) {
var flag by remember { mutableStateOf(false) }
Box(
Modifier
.fillMaxSize()
.background(Color.DarkGray),
contentAlignment = Alignment.Center
) {
val animate = remember { Animatable(32.dp, Dp.VectorConverter) }
// Trigger animateTo() by coroutine
LaunchedEffect(key1 = flag) {
animate.animateTo(if (flag) 32.dp else 144.dp)
}
Row(
Modifier
.size(animate.value) // size takes the value in animate.background(Color.Magenta) .clickable { flag = ! flag } ) {} } }Copy the code
First look at Animatable:
androidx.compose.animation.core.Animatable
public constructor Animatable<T, V : AnimationVector>(
initialValue: T,
typeConverter: TwoWayConverter<T, V>,
visibilityThreshold: T?
)
Copy the code
initialValue
It’s easy to understand, passed in as its initial value, so calledValue
That’s what the holder holds.typeConverter
Is used to unify the animation behavior, you can do property animation values through thisconverter
Convert all the different types of values toFloat
Perform animation calculations with the correspondingAnimationVector
Let’s convert them into each other.visibilityThreshold
Judgment animation gradually becomes the target value threshold, can be empty, temporarily press not table.
Take a closer look at the TwoWayConverter:
/** * [TwoWayConverter] class contains the definition on how to convert from an arbitrary type [T] * to a [AnimationVector], and convert the [AnimationVector] back to the type [T]. This allows * animations to run on any type of objects, e.g. position, rectangle, color, etc. */
interface TwoWayConverter<T, V : AnimationVector> {
/** * Defines how a type [T] should be converted to a Vector type (i.e. [AnimationVector1D], * [AnimationVector2D], [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of * type T). */
val convertToVector: (T) -> V
/** * Defines how to convert a Vector type (i.e. [AnimationVector1D], [AnimationVector2D], * [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of type T) back to type * [T]. */
val convertFromVector: (V) -> T
}
Copy the code
TwoWayConverter is used to define a method for converting values of any type to an AnimationVector available for animation, so that it can be encapsulated to perform uniform animation calculations for any property type. At the same time, the wrapper AnimationVectorXD of the corresponding dimension is returned based on the dimension data required by the animation, where XD is the number of dimensionality data. Such as:
androidx.compose.ui.unit.Dp
Values intoAnimationVector
There’s only one dimension, which is itsvalue
And so convert to the correspondingAnimationVector1D
androidx.compose.ui.geometry.Size
Contains two dimensions of data:width
和height
, so the pair is converted toAnimationVector2D
androidx.compose.ui.geometry.Rect
Contains four data dimensions:left
.top
.right
.bottom
, corresponding toAnimationVector4D
Compose also provides a thoughtful default implementation for common and animated objects:
- Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>
- Int.Companion.VectorConverter: TwoWayConverter<Int, AnimationVector1D>
- Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>
- Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
- DpOffset.Companion.VectorConverter: TwoWayConverter<DpOffset, AnimationVector2D>
- Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>
- Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>
- IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>
- IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>
At this point, the Animatable has an initial value and the conversion between the value type and the corresponding animation data, so all you need is a target value to trigger the animation. Since the animation data is computed in the coroutine, all we need to do is fire animateTo() in the coroutine:
// Trigger animateTo() by coroutine
LaunchedEffect(key = flag) {
animate.animateTo(if (flag) 32.dp else 144.dp)
}
Copy the code
Note that the coroutine CoroutineScope is provided via the Composable function LaunchedEffect, which internally optimizes composer and casses state via the remember function. So it will not be executed multiple times due to an active or passive call to Recompose.
AnimationSpec
AnimationSpec, as its name implies, supports defining specifications for animations to implement custom animations.
If you look at the definition of the animateTo function, you can see that its second argument can be set to the animationSpec. It has a default implementation defaultSpringSpec, so the animationSpec is not explicitly specified in the above example.
androidx.compose.animation.core.Animatable
public final suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec,
initialVelocity: T = velocity,
block: (Animatable<T.V- > >. ()Unit)? = null
): AnimationResult<T, V>
Copy the code
spring
DefaultSpringSpec is an animation based on the physical properties of a spring created via Spring:
androidx.compose.animation.core AnimationSpec.kt
@Stable
public fun <T> spring(
dampingRatio: Float = Spring.DampingRatioNoBouncy,
stiffness: Float = Spring.StiffnessMedium,
visibilityThreshold: T? = null
): SpringSpec<T>
Copy the code
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
Copy the code
Spring accepts two parameters, dampingRatio and Boundings. The former defines the elasticity of the Spring, the default value is Spring. The DampingRatioNoBouncy. The latter defines the speed at which the spring moves towards targetVaule. The default value is Spring.StiffnessMedium. Spring cannot set duration based on physical properties. Please refer to the following figure for specific effects:
tween
androidx.compose.animation.core AnimationSpec.kt
@Stable
public fun <T> tween(
durationMillis: Int = DefaultDurationMillis,
delayMillis: Int = 0,
easing: Easing = FastOutSlowInEasing
): TweenSpec<T>
Copy the code
Tween animates between the start and end values using the ease curve within the specified durationMillis. The animation curve was added through Easing.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
Copy the code
keyframes
Keyframes adds animation effects based on snapshot values specified in different timestamps during the animation duration. At any given time, the animation values are interpolated between the two keyframe values. For each of these keyframes, you can specify Easing to determine the interpolation curve:
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0 f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2 f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4 f at 75 // ms
0.4 f at 225 // ms})Copy the code
snapTo(targetValue: T)
androidx.compose.animation.core.Animatable
public final suspend fun snapTo(
targetValue: T
): Unit
Copy the code
Animatable also provides a snapTo(targetValue) function, which allows you to directly set the value it holds internally. This process does not generate any animation and the animation in progress is cancelled. This function can be used for some scenarios that require an initial value before the animation starts.
A more convenient way to use: animate*AsState
Set the target value of an attribute, when the corresponding attribute value changes, automatic trigger animation, transition to the corresponding value.
This Composable function is overloaded for different parameter types such as Float, Color, Offset, etc. When the provided targetValue is changed, the animation will run automatically. If there is already an animation in-flight when targetValue changes, the on-going animation will adjust course to animate towards the new target value.
Compose provides these functions to cover the basic scenario:
- animateFloatAsState
- animateDpAsState
- animateSizeAsState
- animateOffsetAsState
- animateRectAsState
- animateIntAsState
- animateIntOffsetAsState
- animateIntSizeAsState
@Composable
fun Demo(a) {
var flag by remember { mutableStateOf(false) }
Box(
Modifier.fillMaxSize().background(Color.DarkGray),
contentAlignment = Alignment.Center
) {
val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
// You can set the listener function at the end of the animation and call back the target value of the corresponding property at the end of the animation
Log.i(TAG, "size animate finished with $valueOnAnimateEnd") } Row( Modifier .size(size) .background(Color.Magenta) .clickable { flag = ! flag } ) {} } }Copy the code
Demo effect:
The basic operation for compose animation is a little different from the way we normally use animation, and you’ll find that the only way you can affect the core of the animation is by selecting a property and a target value. The initial value of the property cannot even be preset, and the duration of the animation has no effect.
Go a little deeper
Look at the implementation of the animateDpState() function:
/ * * *... . * * [animateDpAsState] returns a [State] object. The value of the state object will continuously be * updated by the animation until the animation finishes. * * Note, [animateDpAsState] cannot be canceled/stopped without removing this composable function * from the tree. See [Animatable] for cancelable animations. * *@sample androidx.compose.animation.core.samples.DpAnimationSample
*
* @param targetValue Target value of the animation
* @param animationSpec The animation that will be used to change the value through time. Physics animation will be used by default.
* @param finishedListener An optional end listener to get notified when the animation is finished.
* @return A [State] object, the value of which is updated by animation.
*/
@Composable
fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
finishedListener: ((Dp) - >Unit)? = null
): State<Dp> {
return animateValueAsState(
targetValue,
Dp.VectorConverter,
animationSpec,
finishedListener = finishedListener
)
}
Copy the code
View the animateIntAsState implementation:
/ * * *... . * * [animateIntAsState] returns a [State] object. The value of the state object will continuously be * updated by the animation until the animation finishes. * * Note, [animateIntAsState] cannot be canceled/stopped without removing this composable function * from the tree. See [Animatable] for cancelable animations. * *@param targetValue Target value of the animation
* @param animationSpec The animation that will be used to change the value through time. Physics
* animation will be used by default.
* @param finishedListener An optional end listener to get notified when the animation is finished.
* @return A [State] object, the value of which is updated by animation.
*/
@Composable
fun animateIntAsState(
targetValue: Int,
animationSpec: AnimationSpec<Int> = intDefaultSpring,
finishedListener: ((Int) - >Unit)? = null
): State<Int> {
return animateValueAsState(
targetValue, Int.VectorConverter, animationSpec, finishedListener = finishedListener
)
}
Copy the code
-
TargetValue is the targetValue of a Dp attribute, meaning that you want the attribute to change to a specific value;
-
AnimationSpec Has a default implementation of how the value of this property changes over time;
-
FinishedListener animation end function, nullable;
-
The anumate*AsState family of functions are implemented similarly, internally calling animateValueAsState(…)
This is a state-based implementation. The way of encapsulating and subscribing data in contact Compose can be considered as: when a certain behavior of the program triggers the animation to start, Compose will automatically start up, calculate the value of the corresponding property according to the time, and then return it through State. The Composable function retrieves the latest value of this property from State to State in the recompose behavior and updates to the interface until the value changes to the target State. That’s the end of the animation.
Further into the implementation of animateValueAsState:
@Composable
fun <T, V : AnimationVector> animateValueAsState(
targetValue: T,
typeConverter: TwoWayConverter<T, V>,
animationSpec: AnimationSpec<T> = remember {
spring(visibilityThreshold = visibilityThreshold)
},
visibilityThreshold: T? = null,
finishedListener: ((T) -> Unit)? = null
): State<T> {
val animatable = remember { Animatable(targetValue, typeConverter) }
val listener by rememberUpdatedState(finishedListener)
val animSpec byrememberUpdatedState(animationSpec) ... .return animatable.asState()
}
Copy the code
You’ll notice that the inside is still implemented using Animatable. Although anumate*AsState is based on Animatable, it does not extend the use of Animatable, but rather limits it. Animate *AsState is designed to encapsulate deterministic simple use scenes that have explicit state changes and require less complex values for animation. In these scenes, it is very useful to be able to quickly define animation, even if the scene becomes complex. The Animatable can also be used as a bottom pocket.
updateTransition
In the actual use of the scene, in many cases, the animation design is not a single parameter can be completed, such as the size change and color transition, size and rounded corner change at the same time, shape and color change at the same time. These situations require combining multiple animations simultaneously:
@Composable
fun Demo(a) {
var flag by remember { mutableStateOf(false) }
Box(
Modifier
.fillMaxSize()
.background(Color.DarkGray),
contentAlignment = Alignment.Center
) {
val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
// You can set the listener function at the end of the animation and call back the target value of the corresponding property at the end of the animation
Log.i(TAG, "size animate finished with $valueOnAnimateEnd")}val color by animateColorAsState(
targetValue = if (flag) MaterialTheme.colors.primary elseMaterialTheme.colors.secondary ) Row( Modifier .size(size) .background(color) .clickable { flag = ! flag } ) {} } }Copy the code
However, there is a problem with the above implementation, that is, the animation process of each attribute value is calculated separately, and each attribute animation has to be managed by a separate state, which is obviously wasteful in performance, but also very inconvenient. This situation can be introduced to Transition for unified animation management:
@Composable
fun Demo(a) {
var flag by remember { mutableStateOf(false) }
Box(
Modifier
.fillMaxSize()
.background(Color.DarkGray),
contentAlignment = Alignment.Center
) {
val transition = updateTransition(flag)
val size = transition.animateDp { if (it) 32.dp else 96.dp }
val color = transition.animateColor { if (it) MaterialTheme.colors.primary elseMaterialTheme.colors.secondary } Row( Modifier .size(size.value) .background(color.value) .clickable { flag = ! flag } ) {} } }Copy the code
In this way, when the Transition state is triggered, the values of size and color can be calculated internally at the same time, saving billions of bits of performance 🤏🏻
@Composable
fun <T> updateTransition(
targetState: T,
label: String? = null
): Transition<T> {
val transition = remember { Transition(targetState, label = label) }
transition.animateTo(targetState)
DisposableEffect(transition) {
onDispose {
// Clean up on the way out, to ensure the observers are not stuck in an in-between
// state.
transition.onTransitionEnd()
}
}
return transition
}
Copy the code
UpdateTransition can create and save states, internally using Remember.