Jetpack Compose Animations I

Gonzalo Campos
Wizeline Mobile
Published in
5 min readApr 5, 2021

--

Jetpack Compose has been promoted from alpha to beta some time ago, which means the API’s last breaking changes are up. One of the greatest changes we can find is in its animations interface, which, in general, makes animations easier to understand, to read, and to build.

In this article, I will make a general overview of this animation API, making a special emphasis on Transitions object, which will help us create beautiful declarative animations in any composable we write by having a good control on what’s being displayed.

State-Driven Animations

If you’ve been into Jetpack Compose for a while, you’ll know that you can have some state in your composables by using the remember and (if the state is mutable) mutableStateOf duple. In case your state is immutable, the remember function will keep that very value in every recomposition; in case it is mutable, it will trigger the recomposition anytime that value changes, because when you use mutableStateOf you’ll receive a MutableState object, which is an observable type.

That recomposition is the key to create state-driven animations. They will be triggered in any change of state of our composable, taking advantage of the recomposition. Let’s see the simplest composable of this type: AnimatedVisibility.

AnimatedVisibility

You just need two things:

  1. Set a variable that will hold the state defining the visibility of the composable:
var visible by remember { mutableStateOf(true) }

2. Wrap any composable you want inside the `AnimatedVisibility` composable:

AnimatedVisibility(visible = visible) {
Box(modifier = Modifier
.size(200.dp)
.background(Color.Blue))
}

So, anytime the value visible changes, the AnimatedVisibility composable will catch that change and manage the whole animation for you! Let’s say that you change the state via a Button composable:

Button(onClick = { 
visible = !visible
})
Figure 1. AnimatedVisibility in action!

IMPORTANT: right now, the AnimatedVisibility composable is an experimental feature, and such code must include the @ExperimentalAnimationApi annotation or it won’t compile.

animateContentSize()

This is exactly the same principle of the Modifier.animateContentSize() function.

  1. Set a state with a Size value:
var size by remember { mutableStateOf(Size(100F, 100F)) }

2. Add the animateContentSize to our composable:

Box(
modifier = Modifier
.animateContentSize()
.size(size.height.dp)
.background(Color.Red))

3. Change the size variable in a Button:

Button(onClick = {
size = if (size.height == 100F)
Size(200F, 200F)
else
Size(100F, 100F)
})
Figure 2. animateContentSize() modifier.

animateFooAsState

Finally, let me introduce the animateFooAsState, which follows the same principle of the state-driven animations and will animate the respective property when the state of the composable changes.

First of all, Jetpack Compose offers a lot of built-in functions that can animate different types of data. For example, you have: animateColorAsState, animateDpAsState, animateOffsetAsState, etc.

As in the previous examples, you need:

1. A piece of state that will change and trigger our animation:

var rotate by remember { mutableStateOf(false) }

2. Based on this rotate variable, set the targetValue that our animation is going to have:

val rotationAngle by animateFloatAsState(
targetValue = if (rotate) 360F else 0F
)

3. Change our state by using a Button:

Button(onClick = {
rotate = !rotate
})
Figure 3. animateFloatAsState()

Transitions

Although AnimatedVisibility, animateContentSize and animateFooAsState can be very helpful and easy to use, they are pretty limited and they animate just one property at a time, letting us with a little monotone animations. We can apply the same principle of changing the state to create animations by using the Transition object, which is way more flexible. They allow us to do something like this:

Figure 4. Transitions power!

Let’s take a look at the code.

For this example, you create a custom enum class that will be part of the state of our composable:

enum class BoxPosition {
TopRight,
TopLeft,
BottomRight,
BottomLeft
}
var boxPosition by remember { mutableStateOf(BoxPosition.TopLeft) }

This boxPosition will be in charge of controlling the position of the box in our composable. However, if you just add the logic to move the box with no animations, the UI would look pretty awkward and lifeless. You can use the updateTransition function to create some animations!

The updateTransition animation will receive a targetState parameter, that will be listened by the transition to trigger the animation in your composable:

val transition = updateTransition(targetState = boxPosition)

Now, the transition value is a Transition object, and it includes a method called animateFoo (very similar to animateFooAsState). Its main argument is a lambda that will receive an object of the type of our targetState (in this case, a BoxPosition), and you need to return the value you want it to take in a certain point. For this example I used animateOffset, and, in the case of BoxPosition.TopLeft, you need the box to be in the top-left corner of our container, so you need to return an Offset with both x and y equals to 0. You need to cover all of the cases in our BoxPosition class:

val boxOffset by transition.animateOffset { position ->
when (position) {
BoxPosition.TopLeft -> Offset(0F, 0F)
BoxPosition.BottomRight -> Offset(120F, 120F)
BoxPosition.TopRight -> Offset(120F, 0F)
BoxPosition.BottomLeft -> Offset(0F, 120F)
}
}

If you pay attention to the boxOffset variable, it is of the type State<T>, which means it can be used as a normal value anywhere!

Box(modifier = Modifier
.offset(boxOffset.x.dp, boxOffset.y.dp)
.size(30.dp)
.background(Color.Yellow))

And once again, it’s a Button that will help you to change the state of your composable:

Button(onClick = {
boxPosition = getNextPosition(boxPosition)
})
fun getNextPosition(position: BoxPosition) =
when (position) {
BoxPosition.TopLeft -> BoxPosition.BottomRight
BoxPosition.BottomRight -> BoxPosition.TopRight
BoxPosition.TopRight -> BoxPosition.BottomLeft
BoxPosition.BottomLeft -> BoxPosition.TopLeft
}

The getNextPosition seems a little bit like a state machine, and you can change the order of our animation with it.

Conclusion

Jetpack Compose has simplified animations to the point of creating declarative code in our composables: you just write how you want the UI to animate and the rest is managed by Compose. At the end, this was the main goal of Jetpack Compose: create a declarative UI toolkit to accelerate the app development and improve the code readability and logic.

There is more content about animations such as target based animations, keyframes, tweens, springs and more. But they’ll be explored in the next one.

You can find all the examples above in this repo.

Cheers!

--

--