Jetpack Compose Animations Tutorial: Getting Started
In this tutorial, you’ll build beautiful animations with Jetpack Compose Animations, and discover the API that lets you build these animations easily. By Andres Torres.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Jetpack Compose Animations Tutorial: Getting Started
30 mins
- Getting Started
- Setting up Your Component
- Working With Jetpack Compose Animations
- Making the Button Change Width
- Making Your Component React to Value Changes
- Reversing the Animation
- Rounding the Corners of the Pressed State
- Changing Colors Between States
- Fading in/out Button Content
- Animating Idle State Using Keyframes
- Animating Pressed State Using Repeatable
- Where to Go From Here?
Jetpack Compose is a new and super awesome toolkit for building native Android UI in a declarative fashion. Building declaratively means the code describes how the UI should look based on the available state, instead of changing the UI every time the state changes. Because of this, Jetpack Compose lets you develop apps using a more modular approach by writing less code and organizing it in smaller and reusable components, which are easier to maintain. But what about Jetpack Compose Animations?
The declarative nature of Jetpack Compose allows the developers to write beautiful and complex animations in an expressive and intuitive way. The flexibility that Jetpack Compose Animations API provides makes it really easy to animate any component’s property to convey information to the user, using meaningful motion! :]
In this tutorial, you’ll learn about Jetpack Compose Animations: how they work behind the scenes, the different options for animating your components, and how to take advantage of them to improve your UI.
In the process, you’ll learn how to:
- Define a start and end state for your transitions.
- Describe your transition in terms of duration and type of animation.
- Use different predefined transition builders, such as tween, repeatable and keyframes.
- Use easing functions to provide more realism or fluidity to your animations.
- Make everything work together in a modular and reusable fashion.
Animations are there to work in favor of the user experience and Jetpack Compose Animations have made this easy. Time to take your app to the next level!
It’s also important to remember that Jetpack Compose is still under development, so it might change in the future. Furthermore, you’ll need Android Studio 4.2 Canary, which is also a bit brittle. Nonetheless, this tutorial will help you prepare for their stable release.
Getting Started
Download the starter project by clicking on the Download Materials button at the top or bottom of the tutorial. Then, open the starter project in Android Studio, where you’ll find Favenimate, a playground for your future animated button.
Then build & run the app. You’ll see the following screen:
This app is just a showcase to animate a button with a very common functionality that can be reused in any number of apps. Right in the middle of the screen is the button you’ll animate. This button doesn’t do anything now, but in this tutorial, you’ll learn how to animate this button from an idle state to a pressed state and backward.
If you inspect the project closely, you’ll notice a special annotation in the MainActivity.kt file, called @Preview
. It is a new annotation, from the Jetpack Compose toolkit, that lets you preview all UI elements within Android Studio. When you build the app at least once, you can interact with individual components, within the IDE. You won’t have to build the app or navigate to certain screens. Simply preview the component! You can see the annotation and special preview controls in the IDE below.
In the first selection, you can see the annotation within MainContent
. Because of this annotation, you can preview your components, using the side menu. In the second selection, you can see the side menu and its options. Right now, you’re using the split option, which lets you preview both the code and the design. This is very similar to what you’re used to from XML.
Finally, in the third selection, you can see something called interactive mode. Using interactive mode, you can interact with your components! This allows you to click your buttons and preview them, focus text input, and much more. You can try it out yourself, by tapping on the interactive icon.
Setting up Your Component
Open AnimatedFavButton.kt. This is the file where you’ll work most of the time. Add the following code at the top of the file:
enum class ButtonState {
IDLE, PRESSED
}
This creates an enum
with the different states of your button, namely an idle and a pressed state. You need this, as you’ll have to differentiate between your component being idle, and the user interacting with it! :]
Next, replace the AnimatedFavButton
function with the following code:
@Preview //1
@Composable //2
fun AnimatedFavButton() {
val buttonState = remember { mutableStateOf(ButtonState.IDLE) } //3
//Transition Definition
FavButton()
}
Here’s a code breakdown:
-
As mentioned before,
@Preview
will allow you to visualize this component in Android Studio, without having to build the entire app. -
@Composable
tells the compiler that the function is a composable function, which will serve as the reusable component for your animated button. - This line defines the initial state of the component as
IDLE
and remembers the state even if the UI component draws again. Because the state is mutable, you can change it, forcing the component to draw with the new state.
Before adding some nice vibes to the button, let’s take a step back to review some theory behind Jetpack Compose Animations.
Working With Jetpack Compose Animations
The simplest way to work with animations in Jetpack Compose is by using the Transition
component. This component lets you define several things for your animations:
- Start and end states.
- The animation timer.
- Callback for when the animation completes.
- Composable content wrapper function, that lets you add UI elements that you want to animate.
Besides defining this Transition
component, you need to also define the start and end state properties as well as describe how these properties must be animated in terms of duration and behavior. This might sound like a lot, but it’s a fairly easy and straightforward process. To show you how easy, let’s start animating the width of the button. :]
To maintain modularity and simplicity in the project, create a new file called AnimPropKeys.kt, in the UI
package, with the following code:
import androidx.compose.animation.DpPropKey
val width = DpPropKey()
Jetpack Compose Animations are achieved through the update of PropKey
s. A PropKey
refers to a property and an animation vector you want to animate. You can easily create your own custom PropKey
s but Jetpack Compose Animations come with several built-in PropKey
s that make the work incredibly easy. You’ll explore a few of them in this tutorial.
As you might quickly realize, DpPropKey
is a built-in PropKey
that animates density pixel values. And because you’re going to animate the width of the button, this key works like a charm. :]
Making the Button Change Width
Now, you’ll learn how to define a transition animation for your component. In AnimatedFavButton.kt, replace the line with FavButton()
with the following code:
//1
val transitionDefinition = transitionDefinition<ButtonState> {
//2
state(ButtonState.IDLE) {
this[width] = 300.dp
}
//3
state(ButtonState.PRESSED) {
this[width] = 60.dp
}
//4
transition(fromState = ButtonState.IDLE, toState = ButtonState.PRESSED) {
width using tween(durationMillis = 1500)
}
}
//5
val state = transition(
definition = transitionDefinition,
initState = buttonState.value,
toState = ButtonState.PRESSED
)
FavButton(buttonState = buttonState, state = state)
Wow, that’s definitely a lot of code, but I promise it’ll be worth it! :]
Let’s dive bit by bit into this code:
- You create a variable for the
transitionDefinition
of your component. - Next, you declare an initial state where you tell the
transitionDefinition
that it will hold your previously defined widthDpPropKey
with a value of 300 dp for theIDLE
state. - Then you declare your final pressed state the same way you did for the initial state, but this time the value of the width
DpPropKey
will be 60 dp. - Following the declaration of the states, you declare the actual transition as an animation that goes from the values set in the initial state to the final state. You tell Jetpack Compose Animations the width
DpPropKey
will be animated usingtween()
and that the whole animation will take 1,500 milliseconds.tween()
component extendsDurationBasedAnimationBuilder
, which in turn extendsAnimationBuilder
and builds the tween animation from a start and end value, based on an easing curve and a duration. Other types ofAnimationBuilder
s you can work with to recreate different animations arePhysicsBuilder
,RepeatableBuilder
,SnapBuilder
andKeyframesBuilder
. You’ll work with some of them later in this tutorial. - Finally, you build the animation using
transition()
. Then you use the return value of the transition as aTransitionState
, and pass it, and thebuttonState
to theFavButton
.
You need to import the following packages to remove the compile errors:
import androidx.compose.animation.core.transitionDefinition
import androidx.compose.animation.core.tween
import androidx.compose.animation.transition
import androidx.compose.ui.unit.dp
You may notice a compile error on the signature of FavButton
. Don’t worry about it, you’ll fix it in a moment.
transition
within transitionDefinition
is very different from the second transition
component. The transition()
within transitionDefinition
defines the animation from one state to another, while the second transition()
takes the transitionDefinition
, target state, and builds a TransitionState
. You then use the state to read the value you animate, such as the button width in your case.
Making Your Component React to Value Changes
You almost have everything you need in terms of defining your animation. The last bit of code will go on the actual button so it uses the values that come from the state provided by the transition()
rather than fixed values. Open FavButton.kt and replace the code with the following:
@Composable
fun FavButton(buttonState: MutableState<ButtonState>, state: TransitionState) { //line changed
Button(
border = Border(1.dp, purple500),
backgroundColor = Color.White,
shape = RoundedCornerShape(6.dp),
modifier = Modifier.size(state[width], 60.dp), //line changed
onClick = {}
) {
ButtonContent()
}
}
You can fix the missing references using Alt+Enter in Android Studio, to import the types. You can see that changes occurred in just two lines.
The first change happened within the signature, where now you pass the state that will hold the values for each of your defined properties on each frame of the animation and the current button state, which you’ll use later in the tutorial, as parameters.
The second change happened on the size modifier, where instead of providing a fixed value for the width value, you are dynamically setting it up to be the value of the DpPropKey
in the state parameter.
Now, let’s look at what you have achieved. Build and run the project. Your button automatically transitions up from the idle to the pressed state, and in turn changes its width! :]
Congratulations! You’ve achieved quite a lot. Now that all the necessary components are in place, you can start having more fun with other types of animations. :]
Reversing the Animation
Wouldn’t it be cool for your button to animate itself or revert to the previous state based on a button click? With just a bit of tweaking and declaring a transition from the pressed to the idle state, you can achieve that in Jetpack Compose Animations. Open AnimatedFavButton.kt and define the following transition
within the composable function:
// 5
transition(ButtonState.PRESSED to ButtonState.IDLE) {
width using tween(durationMillis = 1500)
}
Like you did before, this describes the behavior and duration of the PropKey
that animate from a pressed to an idle state.
Now, replace everything underneath the transitionDefinition
with this:
// 1
val toState = if (buttonState.value == ButtonState.IDLE) {
ButtonState.PRESSED
} else {
ButtonState.IDLE
}
val state = transition(
definition = transitionDefinition,
initState = buttonState.value,
toState = toState // 2
)
FavButton(buttonState, state = state)
Here, you have two important changes. You first created a toState
value, to store the appropriate state, based on the initial buttonState
. Now, if the button is in IDLE
state, the toState
parameter will be PRESSED
and vice-versa. :]
And then you passed that value to the transition()
so that the animation supports both ways of animating. It's very easy to do in Jetpack Compose Animations.
Finally, you need to toggle the button state value when the user clicks the button! Open FavButton.kt and inside onClick()
, add the following code:
buttonState.value = if (buttonState.value == ButtonState.IDLE) {
ButtonState.PRESSED
} else {
ButtonState.IDLE
}
With this code, you're toggling the button state, when the user taps it.
Build and run. You can see the first animation from an idle to a pressed state go off. But now when you click the button, it actually goes to its previous state. Do it a couple of times and see how nice it looks!
Jetpack Compose Animations are just awesome, aren't they? :]
Rounding the Corners of the Pressed State
Now that you have the basic skeleton for going forward and backward with the button transitions built with Jetpack Compose Animations, you'll reinforce the previously acquired knowledge by changing the shape of the button with the pressed state. To do that, you'll animate the rounded corners property of the button. The rounded corners property of the button can be measured in multiple values.
You can measure it in dp, but this seems to be buggy, as not all corners receive the same radius. You can measure it in percentage, which seems to be working, to create a rounded button. And finally, you can measure it in a float amount of pixels. For your example, you'll use a percentage, as that's the most intuitive way of thinking.
Add a new IntPropKey
by opening the AnimPropKeys.kt file and adding the following code:
val roundedCorners = IntPropKey()
Then open AnimatedFavButton.kt and replace transitionDefinition
with the following:
val transitionDefinition = transitionDefinition<ButtonState> {
state(ButtonState.IDLE) {
this[width] = 300.dp
this[roundedCorners] = 6 // new code
}
state(ButtonState.PRESSED) {
this[width] = 60.dp
this[roundedCorners] = 50 // new code
}
transition(ButtonState.IDLE to ButtonState.PRESSED) {
width using tween(durationMillis = 1500)
// begin new code
roundedCorners using tween(
durationMillis = 3000,
easing = FastOutLinearInEasing
)
// end new code
}
transition(ButtonState.PRESSED to ButtonState.IDLE) {
width using tween(durationMillis = 1500)
// begin new code
roundedCorners using tween(
durationMillis = 3000,
easing = FastOutLinearInEasing
)
// end new code
}
}
Here, you've added idle and pressed state values for your roundedCorners
property and defined an animation builder and duration in each of the transitions. Make sure to import FastOutLinearInEasing
!
It's important to notice that these numbers go from 6 to 50, and are measured in percent (%) of the radius of your corners.
Finally, open FavButton.kt and replace the shape
parameter of the button, using this new line:
shape = RoundedCornerShape(state[roundedCorners]),
This dynamically changes the value of the rounded corners property on each frame. Again, make sure to import the roundedCorners
prop key.
One interesting property of TweenBuilder
is the ability to define a CubicBezierEasing
curve that modifies the behavior of the animation. For the roundedCorners
property, you add a FastOutLinearInEasing
curve, which animates the elements by starting at rest and ending at peak velocity. Other easing curves you can use are FastOutSlowInEasing
, LinearOutSlowInEasing
or LinearEasing
. Try them out and see what effects you can achieve in different properties of your Jetpack Compose Animations!
Now, build and run the app. Notice how the button has a nice rounded shape in the pressed state!
Changing Colors Between States
Changing the shape of your button is something that will quickly grab the user's attention, but you can do even better. Color changes can also quickly remind the user that something just happened and they might need to pay attention.
For your button, you'll invert the colors from one state to the other. Luckily for you, changing colors is just as easy as changing the width or the shape of something. Open AnimPropKeys.kt and add the following code to the end of the file:
val backgroundColor = ColorPropKey()
val textColor = ColorPropKey()
Make sure to add any missing imports! Here, you use another built-in property key but this one, instead of updating dp values, will update... you've guessed it, color values!
Now, move to AnimatedFavButton.kt, and add to the idle state:
this[textColor] = purple500
this[backgroundColor] = Color.White
Make sure to add the following imports to avoid errors:
import androidx.ui.graphics.Color
import com.raywenderlich.android.favenimate.ui.purple500
Next, add the following to the pressed state:
this[textColor] = Color.White
this[backgroundColor] = purple500
Add to the transition from idle to pressed:
backgroundColor using tween(durationMillis = 3000)
textColor using tween(durationMillis = 500)
And finally, add the same code, to the pressed transition:
backgroundColor using tween(durationMillis = 3000)
textColor using tween(durationMillis = 500)
You should be familiar with what you just did! You simply set up some values for different PropKeys
as well as their behavior and duration on specific transitions. To make everything come together, you need to pass the state and the button state to the actual button content like you did for the button. Open ButtonContent.kt and change the signature of the ButtonContent
to the following:
fun ButtonContent(
buttonState: MutableState<ButtonState>,
state: TransitionState
) {
// the body does not change
}
Finally, you need to update the properties to their state values rather than fixed values. Update the color
parameter of the Text
and the tint
parameter of the favorite border Icon
to state[textColor]
. The resulting code should look like this:
@Composable
fun ButtonContent(
buttonState: MutableState<ButtonState>,
state: TransitionState
) {
Row(verticalGravity = Alignment.CenterVertically) {
Column(
Modifier.width(24.dp),
horizontalGravity = Alignment.CenterHorizontally
) {
Icon(
tint = state[textColor], // new code
asset = Icons.Default.FavoriteBorder,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
"ADD TO FAVORITES!",
softWrap = false,
color = state[textColor] // new code
)
}
}
On FavButton.kt, update the backgroundColor
parameter to state[backgroundColor]
. Also, remember to pass the correct parameter to ButtonContent
due to the change of signature:
Button(
border = BorderStroke(1.dp, purple500),
backgroundColor = state[backgroundColor],
...
) {
ButtonContent(buttonState = buttonState, state = state)
}
Build and run. Check it out!
Look how gracefully your button transitions from one color to another! Even though you're only setting up an initial and final color, Jetpack Compose Animations are smart enough to know how to transition smoothly between them. :]
Fading in/out Button Content
Before doing more fancy stuff, you'll work with another built-in key, the FloatPropKey
. As its name implies, this key updates float values. You'll fade in and out the opacity of the content of the button with this key. Add the following values to AnimPropKeys.kt:
val textOpacity = FloatPropKey()
val iconOpacity = FloatPropKey()
You have just set two property keys for the text and icon opacity.
On AnimatedFavButton.kt, add the following code to the idle state:
this[textOpacity] = 1f
this[iconOpacity] = 0f
To the pressed state:
this[textOpacity] = 0f
this[iconOpacity] = 1f
To the transition from idle to pressed:
textOpacity using tween(durationMillis = 1500)
iconOpacity using tween(durationMillis = 1500)
And to the transition from pressed to idle:
textOpacity using tween(durationMillis = 3000)
iconOpacity using tween(durationMillis = 3000)
Open ButtonContent.kt and replace ButtonContent
with the following:
@Composable
fun ButtonContent(
buttonState: MutableState<ButtonState>,
state: TransitionState
) {
if (buttonState.value == ButtonState.PRESSED) { //1
Row(verticalGravity = Alignment.CenterVertically) {
Column(
Modifier.width(24.dp),
horizontalGravity = Alignment.CenterHorizontally
) {
Icon(
tint = state[textColor],
asset = Icons.Default.FavoriteBorder,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
"ADD TO FAVORITES!",
softWrap = false,
modifier = Modifier.drawOpacity(state[textOpacity]), //2
color = state[textColor]
)
}
} else {
Icon( //3
tint = state[textColor],
asset = Icons.Default.Favorite,
modifier = Modifier.size(48.dp).drawOpacity(state[iconOpacity]) //4
)
}
}
Add the following imports to avoid errors:
import androidx.ui.material.icons.filled.Favorite
import androidx.ui.core.drawOpacity
Here is a breakdown of the previous code:
- Based on the state of the button, you switch the component that works as the content of the button, either a
Row
or anIcon
. - You add a modifier to the text that will slowly fade it in/out based on the button state.
- You add a full heart icon that will appear when the button is in pressed state.
- This is a modifier on the icon that will slowly fade it in/out based on the button state.
Build and run. See how the transparency changes in your components seamlessly, providing an incredibly smooth experience to the user.
Jetpack Compose Animations once again prove how easy it is to build beautiful UI, with meaningful motion! :]
Animating Idle State Using Keyframes
In the last two sections of this tutorial, you'll truly elevate your component by adding some custom animations with the help of some unexplored, built-in animation builders. In this section, you'll animate the pressed state of your button to give a wink to the user. :]
Open AnimPropKeys.kt file and add:
val pressedHeartSize = DpPropKey()
Now, open AnimatedFavButton.kt and, in both idle and pressed states, add:
this[pressedHeartSize] = 48.dp
Add the following code to the transition from idle to pressed:
pressedHeartSize using keyframes {
durationMillis = 2200
48.dp at 1700
12.dp at 1900
}
The keyframes
builder allows you to define an exact value for a specific frame of the animation. Through pairs of values and durations, you define the needed value on specific frames of the animation. Note that Jetpack Compose Animations will transition smoothly between the start, end, and defined states. If you see an error with the durationMillis
property saying it should be greater than 0, just ignore it. It seems to be a bug with Android Studio, but you should be able to run the code.
Open ButtonContent.kt file and replace the modifier for the favorite Icon
with:
modifier = Modifier.size(state[pressedHeartSize]).drawOpacity(state[iconOpacity])
Now, you can build and run your project. Check out the quick "wink" the pressed state gives you. :]
Animating Pressed State Using Repeatable
Last, but not least, you'll give a final touch to the idle state of the button to draw the user's attention. Open AnimPropKeys.kt file and add:
val idleHeartIconSize = DpPropKey()
Then, open AnimatedFavButton.kt and, in both the idle and pressed states, add:
this[idleHeartIconSize] = 24.dp
Add the following code to the transition from pressed to idle:
idleHeartIconSize using repeatable( // 1
animation = keyframes { //2
durationMillis = 2000
24.dp at 1400
12.dp at 1500
24.dp at 1600
12.dp at 1700
},
iterations = Infinite
) //3
There is a lot happening here in this small chunk of code, so let's see what it does:
- You define an animation builder for your newly added property key of
repeatable
. This builder allows the animation to be repeated certain number of times. - You describe the animation that will be repeated, using keyframes.
- You set the number of iterations as
Infinite
because you want this animation to repeat itself constantly. This parameter also accepts integer values, in case you want to just repeat the animation a specific number of times.
Now, open ButtonContent.kt file and replace the modifier
in the favorite border icon with:
Icon(
tint = state[textColor],
asset = Icons.Default.FavoriteBorder,
modifier = Modifier.size(state[idleHeartIconSize]) // here
)
By reading the state from the idleHeartIconSize
, Jetpack Compose Animations will constantly update the size of the icon, making it smaller and bigger, and simulating a heartbeat!
Build and run your app. Now your app's idle state shows a beautiful beating heart, luring your users to press the button!
Where to Go From Here?
You can download the final version of this project using the Download Materials button at the top or bottom of this tutorial.
Great job completing this tutorial! It wasn't easy but you've learned a lot and now can harness the full power of animations in Jetpack Compose Animations and start putting the cherry on top on those apps of yours. :]
Jetpack Compose is still in developer preview, so your best bet to always be updated on new features or breaking changes is with the official Jetpack Compose documentation. You can also head over to the official examples repository from Google, to check out beautiful applications done fully in Jetpack Compose!
If you have any questions, comments, or want to showcase more beautiful animations, feel free to join the discussion below!