1. Before you begin
In this codelab, you learn how to add a simple animation to your Android app. Animation can make your app more interactive, interesting, and easier for users to interpret. Animating individual updates on a screen full of information can help the user see what changed.
There are many types of animations that can be used in an app user interface. Items can fade in and out as they appear or disappear, they can move on or off of the screen, or they can transform in interesting ways. This helps make the app's UI expressive and easy to use.
Animations can also add a polished look to your app, which gives it an elegant look and feel, and also helps the user at the same time:
Animations that reward the user for a task can make key moments in the user journey more meaningful. | |
Animated elements that respond to keypad input provide feedback to show if the action was successful. | |
Animated list items are placeholders that convey that the content is loading. | |
An animated swipe-to-open action invites and encourages the required gesture. | |
Animated icons can playfully complement or add to the icon's meaning. |
Prerequisites
- Knowledge of Kotlin, including functions, lambdas, and stateless composables.
- Basic knowledge of how to build layouts in Jetpack Compose.
- Basic knowledge of how to create lists in Jetpack Compose.
- Basic knowledge of Material Design.
What you'll learn
- How to build a simple spring animation with Jetpack Compose.
What you'll build
- You will build on the Woof app from the Material Theming with Jetpack Compose codelab, and add a simple animation to acknowledge the user's action.
What you'll need
- The latest version of Android Studio.
- Internet connection to download starter code.
2. Watch the code-along video (Optional)
If you'd like to watch one of the course instructors complete the codelab, play the below video.
It's recommended to expand the video to full screen (with this icon in the lower right corner of the video) so you can see Android Studio and the code more clearly.
This step is optional. You can also skip the video and start the codelab instructions right away.
3. App Overview
In the Material Theming with Jetpack Compose codelab, you created a Woof app using Material Design, which displays a list of dogs and their information.
In this codelab, you will add animation to the Woof app. You'll add hobby information, which will display when you expand the list item. You'll also add a spring animation to animate the list item being expanded:
Get the starter code
To get started, download the starter code:
Alternatively, you can clone the GitHub repository for the code:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git $ cd basic-android-kotlin-compose-training-woof $ git checkout material
You can browse the code in the Woof app
GitHub repository.
4. Add expand more icon
The first step in building a spring animation is to add an expand more icon. The expand more icon provides a button for the user to expand the list item.
Icons
Icons are symbols that can help users understand a user interface by visually communicating the intended function. They often take inspiration from objects in the physical world that a user is expected to have experienced. Icon design often reduces the level of detail to the minimum required to be familiar to a user. For example, a pencil in the physical world is used for writing, so its icon counterpart usually indicates create or edit.
|
Material Design provides a number of icons, arranged in common categories, for most of your needs.
Add Gradle dependency
Add the material-icons-extended
library dependency to your project. You will use the Icons.Filled.ExpandLess
and
Icons.Filled.ExpandMore
icons from this library.
- In the project pane, open Gradle Scripts > build.gradle (Module: Woof.app).
- Scroll to the end of the
build.gradle (Module: Woof.app)
file. In thedependencies{}
block, add the following line:
implementation "androidx.compose.material:material-icons-extended:$compose_version"
Add the icon composable
Add a function to display the expand more icon from the Material icons library and use it as a button.
- In
MainActivity.kt
, after theDogItem()
function, create a new composable function calledDogItemButton()
. - Pass in a
Boolean
for the expanded state, a lambda expression for the button click event, and an optionalModifier
as follows:
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
}
- Inside the
DogItemButton()
function, add anIconButton()
composable that accepts anonClick
named parameter, a lambda using trailing lambda syntax, that is invoked when this icon is pressed, and set it to the passed inonClick
argument.
@Composable
private fun DogItemButton(
// ...
) {
IconButton(onClick = onClick) {
}
}
- Inside the
IconButton()
lambda block, add in anIcon
composable with a named parameter calledimageVector
and set it toIcons.Filled.ExpandMore
. This is the icon buttonthat will display at the end of the list item. Android Studio shows you a warning for the
Icon()
composable parameters that you will fix in later steps. - Add the named parameter
tint
, and set the color of the icon toMaterialTheme.colors.secondary
. Add the named parametercontentDescription
, and set it to the string resourceR.string.expand_button_content_description
.
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Filled.ExpandMore,
tint = MaterialTheme.colors.secondary,
contentDescription = stringResource(R.string.expand_button_content_description)
)
}
Display the icon
Display the DogItemButton()
composable by adding it to the layout.
- At the beginning of the
DogItem()
composable function, add avar
to save the expanded state of the list item. Set the initial value tofalse
.
var expanded by remember { mutableStateOf(false) }
- To display the icon button within the list item, in the
DogItem()
composable function at the end of theRow
block, after the call toDogInformation()
, make a call to theDogItemButton()
. Pass in theexpanded
state and an empty lambda for the callback. You will define this lambda function in a later step.
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
DogItemButton(
expanded = expanded,
onClick = { }
)
}
- Build & Refresh the preview in the Design pane.
Notice the expand more button is not aligned to the end of the list item. You will fix that in the next step.
Align the expand more button
To align the expand more button to the end of the list item, you need to add a spacer in the layout with the Modifier.weight()
attribute.
In the Woof app, each list item row contains a dog image, dog information, and an expand more button. You will add a Spacer composable before the expand more button with weight 1f
to properly align the button icon. Since the spacer is the only weighted child element in the row, it will fill the space remaining in the row after measuring the other unweighted child element's length.
Add the spacer to the list item row
- At the end of the
Row
block in theDogItem()
composable function, add aSpacer
. Pass in aModifier
withweight(1f)
. TheModifier.weight()
causes the spacer to fill the space remaining in the row.
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
Spacer(Modifier.weight(1f))
DogItemButton(
expanded = expanded,
onClick = { }
)
}
- Build & Refresh the preview in the Design pane. Notice that the expand more button is now aligned to the end of the list item.
5. Add Composable to display hobby
In this task, you'll add Text
composables to display the dog's hobby information.
- Create a new composable function called
DogHobby()
that takes in a dog's hobby string resource ID and an optionalModifier
. - Inside the
DogHobby()
function, create a column with the following padding attributes to add space between the column and the children composables.
import androidx.annotation.StringRes
@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(
start = 16.dp,
top = 8.dp,
bottom = 16.dp,
end = 16.dp
)
) { }
}
- Inside the column block, add two
Text
composables – one to display the About text above the hobby information, and another to display the hobby information.
- For the About text, set the style as
h3
(Heading 3) and the color asonBackground
. For the hobby information, set the style tobody1
.
Column(
modifier = modifier.padding(
//..
)
) {
Text(
text = stringResource(R.string.about),
style = MaterialTheme.typography.h3,
)
Text(
text = stringResource(dogHobby),
style = MaterialTheme.typography.body1,
)
}
- This is what the completed
DogHobby()
composable function looks like.
@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(
start = 16.dp,
top = 8.dp,
bottom = 16.dp,
end = 16.dp
)
) {
Text(
text = stringResource(R.string.about),
style = MaterialTheme.typography.h3
)
Text(
text = stringResource(dogHobby),
style = MaterialTheme.typography.body1
)
}
}
- To display the
DogHobby()
composable, inDogItem()
, wrap theRow
with aColumn
. Make a call to theDogHobby()
function, passing in thedog.hobbies
as parameter, after theRow
as the second child.
Column() {
Row(
//..
) {
//..
}
DogHobby(dog.hobbies)
}
The complete DogItem()
function should look like this:
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
Card(
elevation = 4.dp,
modifier = modifier.padding(8.dp)
) {
Column() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
Spacer(Modifier.weight(1f))
DogItemButton(
expanded = expanded,
onClick = { expanded = !expanded },
)
}
DogHobby(dog.hobbies)
}
}
}
- Build & Refresh the preview in the Design pane. Notice the dog's hobby displays.
6. Show or hide the hobby on button click
Your app has an expand more button for every list item, but it doesn't do anything yet! In this section, you will add the option to hide or reveal the hobby information when the user clicks the expand more button.
- In the
DogItem()
composable function, in theDogItemButton()
function call, define theonClick()
lambda expression, change theexpanded
boolean state value totrue
when the button is clicked, and change it back tofalse
if the button is clicked again.
DogItemButton(
expanded = expanded,
onClick = { expanded = !expanded }
)
- At the
DogItem()
function, wrap theDogHobby()
function call with anif
check on theexpanded
boolean.
// No need to copy over
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
Card(
//..
) {
Column() {
Row(
//..
) {
//..
}
if (expanded) {
DogHobby(dog.hobbies)
}
}
}
}
In the above code, the dog's hobby information only displays if the value of expanded
is true
.
- The preview can show you what the UI looks like, and you can also interact with it. To interact with the UI preview, click the Interactive Mode button
in the top-right corner of the Design pane. This starts the preview in interactive mode.
- Interact with the preview by clicking the expand more button. Notice the dog's hobby information is hidden and revealed when you click the expand more button.
Notice that the expand more button icon remains the same when the list item is expanded. For a better user experience, you'll change the icon so that ExpandMore
displays the downward arrow , and
ExpandLess
displays the upward arrow .
- In the
DogItemButton()
function, update theimageVector
value based on theexpanded
state as follows:
import androidx.compose.material.icons.filled.ExpandLess
@Composable
private fun DogItemButton(
//..
) {
IconButton(onClick = onClick) {
Icon(
imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
//..
)
}
}
- Run the app on a device or emulator, or use the interactive mode in the preview again. Notice the icon alternates between the
ExpandMore
and the
ExpandLess
icons.
Good job updating the icon!
When you expanded the list item, did you notice the abrupt height change? The abrupt height change doesn't look like a polished app. To resolve this issue, you will next add an animation to your app.
7. Add animation
Animations can add visual cues that notify users about what's going on in your app. They are especially useful when the UI changes state, such as when new content loads or new actions become available. Animations can also add a polished look to your app.
In this section you will add a spring animation to animate the change in height of the list item.
Spring Animation
Spring animation is a physics-based animation driven by a spring force. With a spring animation, the value and velocity of movement are calculated based on the spring force that is applied.
For example, if you drag an app icon around the screen and then release it by lifting your finger, the icon jumps back to its original location by an invisible force.
The following animation demonstrates the spring effect. Once the finger is released from the icon, the icon jumps back, mimicking a spring.
Spring effect
Spring force is guided by the following two properties:
- Damping ratio: The bounciness of the spring.
- Stiffness level: The stiffness of the spring, that is, how fast the spring moves toward the end.
Below are some examples of animations with different damping ratios and stiffness levels.
|
|
|
|
Now, you will add a spring animation to the app!
- In
MainActivity.kt
, inDogItem()
, add amodifier
parameter to theColumn
layout.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
//..
Card(
//..
) {
Column(
modifier = Modifier
){
//..
}
}
}
Observe the DogHobby()
function call in the DogItem()
composable function. The dog's hobby information is included in the composition, based on the expanded
boolean value. The height of the list item changes, depending on whether the hobby information is visible or hidden. You will use the animateContentSize
modifier to add a transition between the new and the old heights.
// No need to copy over
@Composable
fun DogItem(...) {
//..
if (expanded) {
DogHobby(dog.hobbies)
}
}
- Chain the modifier with the
animateContentSize
modifier to animate the size (list item height) change.
import androidx.compose.animation.animateContentSize
Column(
modifier = Modifier
.animateContentSize()
) {
//..
}
With your current implementation, you are animating the list item height in your app. But, the animation is so subtle that it is difficult to discern when you run the app. To resolve this, you will use an optional animationSpec
parameter that lets you customize the animation.
- Add the
animationSpec
parameter to theanimateContentSize()
function call. Set it to a spring animation withDampingRatioMediumBouncy
andStiffnessLow
parameters.
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
Column(
modifier = Modifier
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
)
- Build & Refresh the preview in the Design pane, and use the interactive mode or run your app on an emulator or device to see your spring animation in action.
Rerun the app on your emulator or device and enjoy your beautiful app with animations!
8. (Optional) Experiment with other animations
animate*AsState
The animate*AsState()
functions are one of the simplest animation APIs in Compose for animating a single value. You only provide the end value (or target value), and the API starts animation from the current value to the specified end value.
Compose provides animate*AsState()
functions for Float
, Color
, Dp
, Size
, Offset
, and Int
, to name a few. You can easily add support for other data types using animateValueAsState()
that takes a generic type.
Use the animateColorAsState()
function to animate the color when a list item is expanded.
Hint:
- Declare a color and delegate its initialization to
animateColorAsState()
function. - Set the
targetValue
named parameter, depending on theexpanded
boolean value.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
//..
val color by animateColorAsState(
targetValue = if (expanded) Green25 else MaterialTheme.colors.surface,
)
Card(
//..
) {...}
}
- Set the
color
you declared above as the background modifier to theColumn
.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
//..
Card(
//..
) {
Column(
modifier = Modifier
.animateContentSize(
//..
)
)
.background(color = color)
) {...}
}
9. Get the solution code
To download the code for the finished codelab, you can use this git command:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
Alternatively, you can download the repository as a zip file, unzip it, and open it in Android Studio.
If you want to see the solution code, view it on GitHub.
10. Conclusion
Congratulations! You added a button to hide and reveal information about the dog. You enhanced the user experience using spring animations. You also learned how to use interactive mode in the Design pane.
You can also try a different type of Jetpack Compose Animation. Don't forget to share your work on social media with #AndroidBasics!