Using animation to visualize state changes
An app’s state is app data that may change over time. In a Compose app, state (for example, a color) is represented by State
or MutableState
instances. State changes trigger recompositions. The composable StateChangeDemo()
shows a button and a box. Clicking the button toggles the color of the box between red and white by changing state:
@Composable fun StateChangeDemo() { var toggled by remember { mutableStateOf(false) } val color = if (toggled) Color.White else Color.Red Column( modifier = Modifier .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Button(onClick = { toggled = !toggled }) { Text( stringResource(R.string.toggle) ) } Box( modifier = Modifier .padding(top = 32.dp) .background(color = color) .size(128.dp) ) } }
In this example, color
is a simple immutable variable. It is set each time toggled
(a mutable Boolean
state) changes. This happens inside onClick
. As color
is used with a modifier applied to Box()
(background(color = color)
), clicking the button changes the box color.
If you try the code, the change feels very sudden and abrupt. This is because white and red are not very similar colors. Using an animation will make the change much more pleasant. Let’s see how this works.
Animating single value changes
To animate a color, you can use the built-in animateColorAsState()
composable. Just replace the val color = if (toggled) …
assignment inside StateDemo()
with the following code block. If you want to try it out, you can have a look at the SingleValueAnimationDemo()
composable. It’s part of the AnimationDemo
example app:
val color by animateColorAsState( targetValue = if (toggled) Color.White else Color.Red )
animateColorAsState()
returns a State<Color>
instance. Whenever targetValue
changes, the animation will run automatically. If the change occurs while the animation is in progress, the ongoing animation will adjust to match the new target value.
Tip
Using the by
keyword, you can access the color state like you can with ordinary variables.
You can provide an optional listener to get notified when the animation is finished. The following line of code prints the color that matches the new state:
finishedListener = { color -> println(color)}
To customize your animation, you can pass an instance of AnimationSpec<Color>
to animateColorAsState()
. The default value is colorDefaultSpring
, a private value in SingleValueAnimation.kt
:
private val colorDefaultSpring = spring<Color>()
spring()
is a top-level function in AnimationSpec.kt
(which belongs to the android.compose.animation
package). It receives a damping ratio, a stiffness, and a visibility threshold. The following line of code makes the color animation very soft:
animationSpec = spring(stiffness = Spring.StiffnessVeryLow)
spring()
returns SpringSpec
. This class implements the FiniteAnimationSpec
interface, which in turn extends AnimationSpec
. This interface defines the specification of an animation, which includes the data type to be animated and the animation configuration. In this case, it is a spring metaphor, though there are others. We will be returning to this interface in the Spicing up transitions through visual effects section. Next, we look at animating multiple value changes.
Animating multiple value changes
In this section, I will show you how to animate several values at once upon a state change. The setup is similar to StateDemo()
and SingleValueAnimationDemo()
: a Column()
composable contains a Button()
composable and a Box()
composable. However, this time, the content of the box is Text()
. The button toggles a state, which starts the animation. The following version of MultipleValuesAnimationDemo()
does not yet contain an animation. It will be inserted below the comment reading FIXME: animation
setup missing
:
@Composable fun MultipleValuesAnimationDemo() { var toggled by remember { mutableStateOf(false) } // FIXME: animation setup missing Column( modifier = Modifier .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Button(onClick = { toggled = !toggled }) { Text( stringResource(R.string.toggle) ) } Box( contentAlignment = Alignment.Center, modifier = Modifier .padding(top = 32.dp) .border( width = borderWidth, color = Color.Black ) .size(128.dp) ) { Text( text = stringResource(id = R.string.app_name), modifier = Modifier.rotate(degrees = degrees) ) } } }
The Box()
shows a black border, whose width is controlled by borderWidth
. To apply borders to your composable functions, just add the border()
modifier. The child of the box, Text()
, is rotated. You can achieve this with the rotate()
modifier. The degrees
variable holds the angle. degrees
and borderWidth
will change during the animation. Here’s how this is done:
val transition = updateTransition( targetState = toggled, label = "toggledTransition" ) val borderWidth by transition.animateDp( label = "borderWidthTransition" ) { state -> if (state) 10.dp else 1.dp } val degrees by transition.animateFloat( label = "degreesTransition" ) { state -> if (state) -90F else 0F }
The updateTransition()
composable function configures and returns a Transition
. When targetState
changes, the transition will run all of its child animations toward their target values. The label
parameter is used to differentiate different transitions in Android Studio. While it is optional, please consider setting it because the transition can then be better inspected in the Animation Preview. Please refer to the Exercise section at the end of this chapter for more information about previewing animations.
Child animations are added using animate…()
functions. They are not part of a Transition
instance but are extension functions. animateDp()
adds an animation based on density-independent pixels.
In my example, it controls the border width. animateFloat()
creates a Float
animation. This function is ideal for changing the rotation of Text()
, which is a Float
value. There are more animate…()
functions, which operate on other data types. For example, animateInt()
works with Int
values. animateOffset()
animates an Offset
instance. You can find them in the Transition.kt
file, which belongs to the androidx.compose.animation.core
package.
Transition
instances provide several properties reflecting the status of a transition. For example, isRunning
indicates whether any animation in the transition is currently running. segment
contains the initial state and the target state of the ongoing transition. The current state of the transition is available through currentState
. This will be the initial state until the transition is finished. Then, currentState
is set to the target state.
As you have seen, it is very easy to use state changes to trigger animations. So far, these animations have modified the visual appearance of one or more composable functions. In the next section, I will show you how to apply animations while showing or hiding UI elements.