Jetpack Compose Animations I
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 Transition
s 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:
- 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
})
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.
- 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)
})
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
})
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:
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!