10
Jetpack Compose Animations
Written by Prateek Prasad
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
So far in this book, you’ve worked on animating views and screens based on the UI toolkit. However, now that Jetpack Compose is gaining in popularity, more and more apps will start migrating to it, so it’s a good idea to know how to animate those apps.
Jetpack Compose offers a host of modern features when building UIs, and it makes things like state management a lot simpler. In this chapter, you’ll learn about animations in Jetpack Compose.
Setting up the project
Open the starter project for this chapter in Android Studio. Build and run. You’ll notice that everything looks the same as in the previous chapter.
The difference lies in the project’s code.
Expand the project structure, and you’ll notice a new package named ui:
The UI package contains three packages:
- components: Contains the components built using Jetpack Compose.
- screen: Contains the screens built using Jetpack Compose and the components mentioned above.
- theme: Contains the color and typography definition used to build the theme for the app.
The rest of the core architecture of the app is still the same, down to the UI scaffold. The three screens of your app still use fragments, but the fragments host composable functions instead of inflating an XML.
With that out of the way, you’ll now dive in and add some sweet animations to this app.
Animating visibility changes
When you open a movie’s details in the app’s current form, you’ll notice that the Cast section snaps into existence as soon as it’s done loading — which feels quite janky.
val visibleState = remember {
MutableTransitionState(initialState = false).apply {
targetState = true
}
}
AnimatedVisibility(
visibleState = visibileState,
enter = fadeIn()
) {
LazyRow(contentPadding = PaddingValues(end = 24.dp)) {
items(it) {
CastItem(it.profilePath)
}
}
}
@ExperimentalAnimationApi
@Composable
fun CastRow(cast: List<Cast>?) {
...
}
Adding a slide-in animation
To make your animation a little more exciting, you’ll now introduce a slide-in animation when the cast row appears.
enter = fadeIn() + slideInVertically()
Animating content sizes
In the app, a few of the movies have lengthy overviews. Unfortunately, these movies’ overviews take up so much space that they push the cast row and the Add to Favorites button off the screen.
Hiding and showing long text
Create a new file named Overview.kt in the components package and create a new composable function, Overview
, that takes in a movie object.
@Composable
fun Overview(movie: Movie) {
}
Overview(movie = movie)
@Composable
fun Overview(movie: Movie) {
Text(
text = movie.overview,
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Start,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
var overviewExpanded by remember { mutableStateOf(false) }
//1
if (movie.overview.length > 200) {
Text(
//2
text = if (overviewExpanded) "READ LESS" else "READ MORE",
style = MaterialTheme.typography.overline,
modifier = Modifier
.padding(24.dp)
.clickable {
//3
overviewExpanded = !overviewExpanded
},
)
}
Text(
text = movie.overview,
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Start,
modifier = Modifier.padding(horizontal = 16.dp),
maxLines = if (overviewExpanded) Int.MAX_VALUE else 4
)
Animating the change in the text
Wrap both Text
composables inside a Column
, as shown below, to animate the text:
@Composable
fun Overview(movie: Movie) {
var overviewExpanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier.animateContentSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = movie.overview,
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Start,
modifier = Modifier.padding(horizontal = 16.dp),
maxLines = if (overviewExpanded) Int.MAX_VALUE else 4
)
if (movie.overview.length > 200) {
Text(
text = if (overviewExpanded) "READ LESS"
else "READ MORE",
style = MaterialTheme.typography.overline,
modifier = Modifier
.padding(24.dp)
.clickable {
overviewExpanded = !overviewExpanded
},
)
}
}
}
Animating state changes
In the details screen of any movie, tapping the Add to Favorites button will bring up a circular progress bar that displays while the operation is in progress:
Giving the button a state
To use updateTransition()
, the button needs a state of its own. Open AddToFavoritesButton.kt and add the following enum
to the top of the file:
enum class ButtonState {
IDLE, PRESSED
}
//1
val buttonState = remember { mutableStateOf(ButtonState.IDLE) }
//2
val transition = updateTransition(buttonState.value, "Button Transition")
//3
val width = transition.animateDp(label = "Button width animation") { state ->
when (state) {
ButtonState.IDLE -> 250.dp
ButtonState.PRESSED -> 56.dp
}
}
Toggling the button’s state
Now that you’ve set up a state for the button, you need to add a mechanism for toggling that state. To do that, you’ll use contentState
, which MovieDetails
observes and passes down as a property.
buttonState.value = if (contentState is Events.Loading) {
ButtonState.PRESSED
} else ButtonState.IDLE
Animating the button
First, get rid of the check that renders theCircularProgressIndicator
when contentState
is Loading
. Remove the if
statement starting with the following including the else
portion, all the way to the }
for the else:
if (contentState is Events.Loading) {
CircularProgressIndicator(
modifier = Modifier.padding(top = 8.dp),
strokeWidth = 2.5.dp,
color = Color.Black
)
} else {
...
}
Button(
modifier = Modifier
.size(250.dp, 56.dp),
shape = RoundedCornerShape(32.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary
),
onClick = { onFavoriteButtonClick(movie) },
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (buttonState.value == ButtonState.PRESSED) {
CircularProgressIndicator(
modifier = Modifier.padding(top = 8.dp),
strokeWidth = 2.5.dp,
color = Color.Black
)
} else {
Icon(
imageVector = if (movie.isFavorite) {
Icons.Default.Favorite
} else {
Icons.Default.FavoriteBorder
},
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = if (movie.isFavorite) {
stringResource(
id = R.string.remove_from_favorites
)
} else {
stringResource(
id = R.string.add_to_favorites
)
},
style = MaterialTheme.typography.button,
maxLines = 1
)
}
}
}
Changing the size of the button
There’s one final thing to sort out before the animation is ready: To make the button shrink and grow, you need to use the width
property you created earlier.
Button(
modifier = Modifier
.size(width.value, 56.dp),
shape = RoundedCornerShape(32.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary
),
onClick = { onFavoriteButtonClick(movie) },
) {
...
}
Challenge: Animating the button color
You animated the button width using the animateDp()
extension available for the Transition
in the button animation.
Key points
- Jetpack Compose introduces a comparatively simple set of APIs to add animations to your app.
-
AnimatedVisibility
lets you animate the visibility changes of a composable. -
AnimatedVisibility
is still an experimental API at the time of this writing, so composables using this need the@ExperimentalAnimationApi
annotation. - To animate content size changes, use
animateContentSize()
on the parent container of a composable. - To trigger based on state changes in your app, use
updateTransition()
. -
Transition
has several convenient extensions. For example,animateDp
andanimateSize
let you animate properties of a composable across state changes.
Where to go from here?
This chapter provided an introduction to the Jetpack Compose animations API. While you covered some of the simple use cases, you barely scratched the surface of what Jetpack Compose offers for animations.