12.
Animating Properties Using Compose
Written by Denis Buketa
Great job on completing the previous chapter. So far, in the third section of this book, you’ve learned how to use ConstraintLayout, build complex UI and react to Compose lifecycles. Those things are certainly fun, but what’s even more fun? Playing with animations! And that’s what you’ll do now. :]
In this chapter, you’ll learn how to:
-
Animate composable properties using
animate(). - Use
transition()to animate multiple properties of your composables. - Animate composable content.
- Implement an animated button to join a subreddit.
- Implement an animated toast that displays when the user joins a subreddit.
Before diving straight into the animation world, you’ll create a composable representing a button that lets users join an imaginary subreddit.
You’ll start by implementing a simple button, like the one shown below:
If a user hasn’t joined the subreddit yet, they can do so by clicking the blue button with the plus icon. If the user is a member already, a white button with a blue check represents that state. Clicking the button again returns it to its previous state.
To follow along with the code examples, open this chapter’s starter project in Android Studio and select Open an existing project.
Next, navigate to 12-animating-properties-using-compose/projects and select the starter folder as the project root. Once the project opens, let it build and sync and you’re ready to go!
Note that if you skip ahead to the final project, you’ll find the completed button with all the animation logic implemented.
Now that you’re all set, it’s time to start coding.
Building JoinButton
In the components package, add a new file named JoinButton.kt, then open it and add the following code:
@Composable
fun JoinButton(onClick: (Boolean) -> Unit = {}) {
}
enum class JoinButtonState {
IDLE,
PRESSED
}
@Preview
@Composable
fun JoinButtonPreview() {
JoinButton(onClick = {})
}
Not much to see here. You just created a root composable for your button and added a preview. Right now, there’s nothing to preview because you haven’t added any content yet.
You also added JoinButtonState, which represents the state of the button, The two options for the state are IDLE or PRESSED.
Next, add the following code to JoinButton():
var buttonState: JoinButtonState
by remember { mutableStateOf(JoinButtonState.IDLE) }
// Button shape
val shape = RoundedCornerShape(corner = CornerSize(12.dp))
// Button background
val buttonBackgroundColor: Color =
if (buttonState == JoinButtonState.PRESSED)
Color.White
else
Color.Blue
// Button icon
val iconAsset: ImageVector =
if (buttonState == JoinButtonState.PRESSED)
Icons.Default.Check
else
Icons.Default.Add
val iconTintColor: Color =
if (buttonState == JoinButtonState.PRESSED)
Color.Blue
else
Color.White
Box(
modifier = Modifier
.clip(shape)
.border(width = 1.dp, color = Color.Blue, shape = shape)
.background(color = buttonBackgroundColor)
.size(width = 40.dp, height = 24.dp)
.clickable(onClick = {
buttonState =
if (buttonState == JoinButtonState.IDLE) {
onClick.invoke(true)
JoinButtonState.PRESSED
} else {
onClick.invoke(false)
JoinButtonState.IDLE
}
}),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = iconAsset,
tint = iconTintColor,
modifier = Modifier.size(16.dp)
)
}
This might look like a lot of code, but you’ll see that it’s pretty simple. Here’s a breakdown, starting from the top.
You first declared a buttonState with remember(). Ideally, you’d represent your state with PostModel, but this simplified approach is enough to demonstrate how animations work.
Next, you used RoundedCornerShape() to define the shape of the button.
You also defined the button’s background color, which will change depending on the buttonState. When the button has JoinButtonState.PRESSED, it will be white. When it’s JoinButtonState.IDLE, it will be blue.
Next, you defined the button’s icon and icon color. When the button’s state is JoinButtonState.PRESSED, you’ll represent the icon with a white plus sign. If it’s JoinButtonState.IDLE, you’ll represent it with a blue check mark.
The last thing you added is the code that emits the button’s UI. You used Box() to define the button shape and background and Icon() to define how the button’s icon will look.
For that code to work, you need to add a few imports as well:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
Great! Now, build the project and check the preview panel.
Note that you can change buttonState’s initial state to PRESSED, to preview the different settings for your button.
Awesome! Next, you’ll add this button to Post().
Adding JoinButton to Post
Before animating JoinButton(), you’ll add it to Post() so you can see it in the app.
Open Post.kt and edit Header() to look like this:
@Composable
fun Header(
post: PostModel,
onJoinButtonClick: (Boolean) -> Unit = {} // here
) {
Row(
modifier = Modifier.padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically // here
) {
Image(
imageResource(id = R.drawable.subreddit_placeholder),
Modifier.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(
R.string.subreddit_header,
post.subreddit
),
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.primaryVariant
)
Text(
text = stringResource(
R.string.post_header,
post.username,
post.postedTime
),
color = Color.Gray
)
}
Spacer(modifier = Modifier.width(4.dp)) // here
JoinButton(onJoinButtonClick) // here
MoreActionsMenu()
}
Title(text = post.title)
}
In the code above, you added:
-
verticalAlignmenttoRow()to center the header content vertically. -
JoinButton()andSpacer()toHeader(). -
onJoinButtonClicktoHeader().
Excellent! Now, build and run the app. Check how your posts look:
Click one of the JoinButtons and you’ll see how the icon and the background change instantly.
Animating the JoinButton background
So far, you’ve made the button background change from one color to another when the state changes. In this section, you’ll animate that transition.
In JoinButton.kt, replace the current definition of buttonBackgroundColor with the following code:
// Button background
val buttonBackgroundColor: Color = animate(
if (buttonState == JoinButtonState.PRESSED)
Color.White
else
Color.Blue
)
Add one more import as well:
import androidx.compose.animation.animate
Here, you wrapped the if clause that defined two different background colors with animate(). By doing that, you implemented the animation between the two colors when the state changes.
With this simple change, you added your first animation. Can you even believe how easy that was? :]
Now, take a closer look at animate(). In the Jetpack Compose documentation, you’ll find a dozen different animate() signatures because the function allows you to animate a dozen different properties out of the box, including Float, Color, Dp, Position and Size. You can even define your own properties.
All those definitions have something in common: You use them for fire-and-forget animations. Once you create a fire-and-forget animation, the app will memorize its position, like other composables. To trigger the animation, or alter the course of the animation, you simply supply a different target to the composable.
Now build and run the app. Click on any JoinButton in the app and notice how the background color changes.
The figure shows how the button looks across several frames of the animation. Notice how the icon changes immediately after the click, while the background slowly transitions from one color to another. What’s really impressive here is how easy it was to implement this animation, which makes your app even nicer!
Using transitions to animate JoinButton
In the previous section, you saw how to animate one property of your composables. Now, you’ll add more content to JoinButton(). This will give you the opportunity to animate several properties at once.
The figure shows how you’ll change JoinButton’s appearance in the JoinButtonState.IDLE state.
Before adding any code, analyze how you’ll accomplish this animation. Which properties do you have to animate? To give the button its new look, you need to:
- Animate the background, as you did in the previous example.
- Change the icon. You’ll change the asset the same way as before, but you’ll improve that change by animating the icon color.
- Hide and show the text depending on the state.
- Animate the button’s width.
So you need to animate four different properties. Keep that in mind when adding the following code.
Defining the transition
To animate these properties, you’ll use transitions. Using transition(), you create state changes between two or more types of state and define state values for each state type. This means you can define the button width, background color, icon and icon color and text for the two state types you have — IDLE and PRESSED.
In JoinButton.kt, add the following code to the top of the file, below the imports:
private val buttonBackgroundColor = ColorPropKey(label = "Button Background Color")
private val buttonWidth = DpPropKey(label = "Button Width")
private val iconTintColor = ColorPropKey(label = "Icon Tint Color")
private val textMaxWidth = DpPropKey(label = "Text Max Width")
To start working with animations, you need to define something known as property or prop keys. A prop key is a uniquely named identifier which describes one property you want to animate. Using ColorPropKey() and DpPropKey() you create keys to animate the button backgroundColor, width, icon tint and text maxWidth properties.
These functions let you define keys for colors and dimensions, but you can also define floating point keys using FloatPropKey().
You also passed in the label property to these keys, which let you inspect and debug those animations in Android Studio.
Now that you’ve defined the keys, create a transitionDefinition by adding the following code under the prop keys:
private val transitionDefinition =
transitionDefinition<JoinButtonState> {
}
Using transitionDefinition() you tell Jetpack Compose you’re building a new transition. You also passed in the JoinButtonState as its type parameter, letting the transition know you’ll animate from any number of JoinButtonState cases.
The definition isn’t worth much if you don’t add some state to it, so add the following code within transitionDefinition():
state(JoinButtonState.IDLE) {
this[buttonBackgroundColor] = Color.Blue
this[buttonWidth] = 70.dp
this[iconTintColor] = Color.White
this[textMaxWidth] = 40.dp
}
state(JoinButtonState.PRESSED) {
this[buttonBackgroundColor] = Color.White
this[buttonWidth] = 32.dp
this[iconTintColor] = Color.Blue
this[textMaxWidth] = 0.dp
}
Within the definition, you gain access to multiple functions that let you define your transition. One of those functions is state(). state() lets you define any number of states that you represent your values and types of animations. They are limited by the type you defined in the generic bounds of transitionDefinition(). In your case, you have the IDLE and PRESSED states.
And within those two state(), you can define any number of prop keys and their values. You then define what the values for the buttonBackgroundColor, buttonWidth, iconTintColor and textMaxWidth should be in respective states.
Finally, you’ll read the transition state using the same keys and depending on the animation progress and which state you’re moving from and to, you’ll get different values to update your UI.
Now that you have these states, define the transitions by adding the following piece of code right after the state()s:
val duration = 600
transition(
fromState = JoinButtonState.IDLE,
toState = JoinButtonState.PRESSED
) {
buttonBackgroundColor using tween(duration)
buttonWidth using tween(duration)
iconTintColor using tween(duration)
textMaxWidth using tween(duration)
}
Here, you used transition() inside a TransitionDefinition to create a TransitionSpec. TransitionSpec defines how to animate from one state to another with a specific animation for each property defined in the states. Currently, the animations supported in a transition are: tween(), keyframes(), spring(), snap() and repeatable().
This chapter won’t cover all of them, but keep in mind that when you don’t define a TransitionSpec, the framework uses the default spring animation for all properties involved, which might not suit your needs.
In your code, you used tween(). With tween(), you created a TweenSpec configured with the given duration, delay and easing curve. Since you only specified a duration, the code uses 0 for delayMillis and FastOutSlowInEasing() for easing.
Easing is a way to adjust an animation’s fraction. The fraction represents how far along the animation you are and its values are within the [0, 1] range, or [0, 100], representing the percent of the animation you finished.
Easing allows transitioning elements to speed up and slow down, rather than moving at a constant, linear, rate. If you want to see the difference between tween() and spring() animations, remove the transition() between JoinButtonState.IDLE and JoinButtonState.PRESSED. You’ll notice how much quicker the spring() is and you’ll see the difference in the speed of the transitioning elements.
Using the code above, you also defined that all the properties will animate when moving from the IDLE to PRESSED state. But you still need to define a transition that goes in the reverse direction — from the PRESSED to IDLE state. Do that by adding the following code underneath:
transition(
fromState = JoinButtonState.PRESSED,
toState = JoinButtonState.IDLE
) {
buttonBackgroundColor using tween(duration)
buttonWidth using tween(duration)
iconTintColor using tween(duration)
textMaxWidth using tween(duration)
}
This transition() represents the reverse animation by swapping the fromState and toState. The animation framework will know what to do and how to do it, based on your state() definitions.
To make Android Studio happy, add the following imports:
import androidx.compose.animation.ColorPropKey
import androidx.compose.animation.DpPropKey
import androidx.compose.animation.core.transitionDefinition
import androidx.compose.animation.core.tween
Next, update JoinButton() by adding the following code just below the buttonState definition:
// Button transition
val transitionState = transition(
definition = transitionDefinition,
toState = buttonState
)
transition() creates a state-based transition using the animation configuration defined in TransitionDefinition. This is especially useful when animating multiple values from one predefined set of values to another. Note how you call this transition() outside of the TransitionDefinition.
transition() starts a new animation or changes the on-going animation when toState changes to a different value. It dutifully ensures that the animation will head towards the new state specified by toState, regardless of the progress of the animation.
If the transition isn’t currently animating, having a new toState value will start a new animation. Otherwise, the running animation will correct course and animate towards the new toState, based on the interruption-handling logic.
transition() takes a transition definition, a target state and child composables. These child composables will receive TransitionState, which captures all the current values of the animation, as an argument. Child composables should read the animation values from TransitionState and apply the value wherever necessary.
Finally, add one more import:
import androidx.compose.animation.transition
Awesome! The definition is done. Since you have to animate a couple of properties, it’s much easier to do so using transition() instead of animate(). Here’s transition()’s signature:
@Composable
fun <T> transition(
definition: TransitionDefinition<T>,
toState: T,
clock: AnimationClockObservable =
AmbientAnimationClock.current,
initState: T = toState,
label: String? = null,
onStateChangeFinished: (T) -> Unit = null
): TransitionState
Above JoinButton(), you defined four different properties that you want to animate. For that, you used PropKey() and the transitionDefinition(), which defines the TransitionDefinition as part of creating a state-based animation.
You then pass that in as a parameter and define that you want to animate to the buttonState.
Great! You’ve now prepared everything you need for your transition, but you still have to connect this code with the composables you want to animate.
Connecting the transition to the composables
First, remove the definitions of buttonBackgroundColor and iconTintColor from JoinButton().
Then, replace the Box() definition with the following:
Box(
modifier = Modifier
.clip(shape)
.border(width = 1.dp, color = Color.Blue, shape = shape)
.background(
color = transitionState[buttonBackgroundColor] // here
)
.size(
width = transitionState[buttonWidth], // here
height = 24.dp
)
.clickable(onClick = {
buttonState =
if (buttonState == JoinButtonState.IDLE) {
onClick.invoke(true)
JoinButtonState.PRESSED
} else {
onClick.invoke(false)
JoinButtonState.IDLE
}
}),
contentAlignment = Alignment.Center
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = iconAsset,
tint = transitionState[iconTintColor], // here
modifier = Modifier.size(16.dp)
)
Text(
text = "Join",
color = Color.White,
fontSize = 14.sp,
maxLines = 1,
modifier = Modifier.widthIn(
min = 0.dp,
max = transitionState[textMaxWidth] // here
)
)
}
}
First, notice how you changed Box()’s content. You used a Row() to align an Icon() and a Text() beside one another.
Second, notice how you access transitionState in Box()’s modifier and how you’re using it for Icon() and Text(). You replaced the properties that you want to animate with transitionState, but you made sure to access the correct property saved in that object. For example, to animate the change in the background color, you used transitionState[buttonBackgroundColor].
Add the following imports as well.
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.Text
import androidx.compose.ui.unit.sp
And that’s it! This is now a complete button that will animate from one state to another.
Build and run the app. You’ll now see the new JoinButton in the posts.
Click the button in any of the posts and see how it animates from one state to the other.
In the figure above, you see how the button’s width and text change as well as the color animations in the button’s background and icon.
Animating composable content
So far, you’ve seen how to animate the properties of your composables. In this section, you’ll explore a different approach to creating animations by learning how to animate composable content.
Note: At the time of writing, this animation API was in an experimental phase, so keep that in mind when you see
@ExperimentalAnimationApiannotations in the code.
In this section, you’ll implement a toast composable that appears when the user joins a subreddit. It will look like this:
This toast will appear any time you join a new subreddit, by tapping the JoinButton. There are a few things you need to do, to implement such behavior, so let’s start by creating the initial toast composable.
Adding JoinedToast
In components, create a new file named JoinedToast.kt. Then, add the following code to it:
@Composable
fun JoinedToast(visible: Boolean) {
ToastContent()
}
@Composable
private fun ToastContent() {
val shape = RoundedCornerShape(4.dp)
Box(
modifier = Modifier
.clip(shape)
.background(Color.White)
.border(1.dp, Color.Black, shape)
.height(40.dp)
.padding(horizontal = 8.dp),
contentAlignment = Alignment.Center
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = vectorResource(
id = R.drawable.ic_planet
)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "You have joined this community!")
}
}
}
@Preview
@Composable
fun JoinedToastPreview() {
JoinedToast(visible = true)
}
Here’s what the code above does. You used a Box() to give your toast a specific background, shape, size and padding. In the Box(), you added a Row() to align an Icon(), Spacer() and Text().
For this to work, add the following imports as well:
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.tooling.preview.Preview
import com.raywenderlich.android.jetreddit.R
Build the project and check the preview panel to see your composable.
Awesome! Next, you’ll animate the toast. :]
Animating JoinedToast
In JoinedToast.kt, replace theJoinedToast() code with the following:
@ExperimentalAnimationApi
@Composable
fun JoinedToast(visible: Boolean) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(initialOffsetY = { +40 }) +
fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
ToastContent()
}
}
Android Studio will complain if you don’t add this import as well.
import androidx.compose.animation.*
As mentioned earlier, @ExperimentalAnimationApi is there because this is an experimental API — at least, at the time of writing.
Here, you wrapped ToastContent() with AnimatedVisibility(), which animates the appearance and disappearance of its content as the visible value changes.
This is AnimatedVisibility()’s signature, taken from the Jetpack Compose documentation:
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
initiallyVisible: Boolean = visible,
content: @Composable () -> Unit
): Unit
You can define different EnterTransition and ExitTransition in enter and exit for the appearance and disappearance animations. There are three types of EnterTransition and ExitTransition: fade, expand/shrink and slide. By using the + sign, you combine the enter and exit transitions. The combination’s order doesn’t matter since the transition animations start simultaneously.
Now, back to your code. You passed visible from JoinedToast() to AnimatedVisibility(). With that, you’ll control when the animation triggers. When visible changes to true, it triggers the enter animation. Otherwise, it triggers the exit animation.
For the enter transition, you combined two transitions: slideInVertically() and fadeIn(). slideInVertically() slides the content vertically from a starting offset defined in initialOffsetY to 0. You control the direction of the slide by configuring initialOffsetY. A positive initial offset means the animation will slide up, whereas a negative value will slide the content down.
For the exit transition, you used slideOutVertically() and fadeOut().
Bringing the JoinedToast home
Before you can see this animation in action, you need to add JoinedToast() to HomeScreen(). You also need to add @ExperimentalAnimationApi to any parent composable of JoinedToast().
Start by adding @ExperimentalAnimationApi to JoinedToastPreview():
@ExperimentalAnimationApi
@Preview
@Composable
fun JoinedToastPreview() {
JoinedToast(visible = true)
}
Next, open HomeScreen.kt and update HomeScreen() like this:
@ExperimentalAnimationApi
@Composable
fun HomeScreen(viewModel: MainViewModel) {
val posts: List<PostModel>
by viewModel.allPosts.observeAsState(listOf())
var isToastVisible by remember { mutableStateOf(false) }
val onJoinClickAction: (Boolean) -> Unit = { joined ->
isToastVisible = joined
if (isToastVisible) {
Timer().schedule(3000) {
isToastVisible = false
}
}
}
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.background(color = MaterialTheme.colors.secondary)) {
items(posts) {
if (it.type == PostType.TEXT) {
TextPost(it, onJoinButtonClick = onJoinClickAction)
} else {
ImagePost(it, onJoinButtonClick = onJoinClickAction)
}
Spacer(modifier = Modifier.height(6.dp))
}
}
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp)
) {
JoinedToast(visible = isToastVisible)
}
}
}
Add the following imports as well:
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import java.util.*
import kotlin.concurrent.schedule
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Alignment
import com.raywenderlich.android.jetreddit.components.JoinedToast
You did a couple of things in the code above. You wrapped a LazyColumn() with a Box(), which allows you to fill the max size of the screen.
You also added a second Box() and added JoinedToast() to its content. This second Box() lets you position JoinedToast() at the bottom. Then, you used remember() to define the visibility state of the toast.
Next, you defined the onJoinClickAction. Tapping any JoinButton triggers onJoinClickAction() and displays a toast. After three seconds, you hide the toast by changing isToastVisible to false.
Finally, you used onJoinClickAction as a parameter for the different posts. However, right now, the TextPost() and ImagePost() don’t have an onJoinButtonClick parameter, so you’ll see an error. You’re going to fix that next.
Adding onJoinButtonClick to the Posts
Open Post.kt and replace TextPost(), ImagePost() and Post() with the following code:
@Composable
fun TextPost(
post: PostModel,
onJoinButtonClick: (Boolean) -> Unit = {}
) {
Post(post, onJoinButtonClick) {
TextContent(post.text)
}
}
@Composable
fun ImagePost(
post: PostModel,
onJoinButtonClick: (Boolean) -> Unit = {}
) {
Post(post, onJoinButtonClick) {
ImageContent(post.image)
}
}
@Composable
fun Post(
post: PostModel,
onJoinButtonClick: (Boolean) -> Unit = {},
content: @Composable () -> Unit = emptyContent()
) {
Card(shape = MaterialTheme.shapes.large) {
Column(
modifier = Modifier.padding(
top = 8.dp,
bottom = 8.dp
)
) {
Header(post, onJoinButtonClick)
Spacer(modifier = Modifier.height(4.dp))
content.invoke()
Spacer(modifier = Modifier.height(8.dp))
PostActions(post)
}
}
}
What’s most important here is that you added onJoinButtonClick to the TextPost, ImagePost and Post signatures and passed it down to the Header(). Excellent work! The header already passes onJoinButtonClick to JoinButton() and handles everything, so you don’t have to update those composables. However, because you’re using an experimental animation API, you need to add appropriate annotations to your composables.
Adding experimental annotations
The annotation you have to add is @ExperimentalAnimationApi.
Open JetRedditApp.kt and add @ExperimentalAnimationApi to the following composables:
MainScreenContainer()AppContent()JetRedditApp()
You can follow Android Studio errors and use quick actions to easily add these imports. Otherwise, find these three functions and paste the following statement at the top of those functions: @ExperimentalAnimationApi.
Add this import as well:
import androidx.compose.animation.ExperimentalAnimationApi
Finally, open MainActivity.kt and add @ExperimentalAnimationApi to onCreate(), like this:
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetRedditApp(viewModel)
}
}
Don’t forget to add an import for @ExperimentalAnimationApi as well:
import androidx.compose.animation.ExperimentalAnimationApi
Whew! Now, build and run the app. Click any JoinButton and observe the toast’s enter and exit animations.
With that, you used three different APIs to animate your composables. Well done!
Key points
- You use
animate()for fire-and-forget animations targeting single properties of your composables. This is very useful for animating size, color, alpha and similar simple properties. - You use
transition()for state-based transitions using the animation configuration defined inTransitionDefinition. - Use
transition()s when you have to animate multiple properties of your composables, or when you have multiple states between which you can animate. - Transitions are very good when showing content for the first time or leaving the screen, menu, option pickers and similar. They are also great when animating between multiple states when filling in forms, selecting options and pressing buttons!
- You use
AnimatedVisibility()when you want to animate the appearance and disappearance of composable content. -
AnimatedVisibility()lets you combine different types of visibility animations and lets you define directions if you use predefined transition animations.
Hopefully, this was a fun ride for you. You had the chance to play with three different APIs to create some simple, yet beautiful animations. What follows is the last chapter of this book. You’ve come a long way indeed!
In the next chapter, you’ll see how to combine the old View framework with Jetpack Compose and how both can coexist in the same codebase.
See you there! :]