Simple Animation with Jetpack Compose

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 This symbol shows 4 corners on a square highlighted, to indicate full screen mode. 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.

7252aa244a54ad90.png

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:

1e9cf1dbc490924a.gif

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 f88173321938c003.png icon. The expand more icon provides a button for the user to expand the list item.

9fbd3fb0daf35fd3.png

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.

Photo by Angelina Litvin on Unsplash

Black and white pencil icon

Material Design provides a number of icons, arranged in common categories, for most of your needs.

bfdb896506790c69.png

Add Gradle dependency

Add the material-icons-extended library dependency to your project. You will use the Icons.Filled.ExpandLess 30c384f00846e69b.png and Icons.Filled.ExpandMore f88173321938c003.png icons from this library.

  1. In the project pane, open Gradle Scripts > build.gradle (Module: Woof.app).

f7fe58e936bbad3e.png

  1. Scroll to the end of the build.gradle (Module: Woof.app) file. In the dependencies{} 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.

  1. In MainActivity.kt, after the DogItem() function, create a new composable function called DogItemButton().
  2. Pass in a Boolean for the expanded state, a lambda expression for the button click event, and an optional Modifier as follows:
@Composable
private fun DogItemButton(
    expanded: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) { 

}
  1. Inside the DogItemButton() function, add an IconButton() composable that accepts an onClick named parameter, a lambda using trailing lambda syntax, that is invoked when this icon is pressed, and set it to the passed in onClick argument.
@Composable
private fun DogItemButton(
   // ...
) {
   IconButton(onClick = onClick) {

   }
}
  1. Inside the IconButton() lambda block, add in an Icon composable with a named parameter called imageVector and set it to Icons.Filled.ExpandMore. This is the icon button f88173321938c003.png that 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.
  2. Add the named parameter tint, and set the color of the icon to MaterialTheme.colors.secondary. Add the named parameter contentDescription, and set it to the string resource R.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.

  1. At the beginning of the DogItem() composable function, add a var to save the expanded state of the list item. Set the initial value to false.
var expanded by remember { mutableStateOf(false) }
  1. At the end of the DogItem() composable function's Row block, call the DogItemButton() function, and then pass in an expanded state and empty lambda for the callback. This code displays the icon button in the list item.
  2. To display the icon button within the list item, in the DogItem() composable function at the end of the Row block, after the call to DogInformation(), make a call to the DogItemButton(). Pass in the expanded 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 = { }
   )
}
  1. Build & Refresh the preview in the Design pane.

a49643f08701a8d.png

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.

6c2b523849f0f626.png

Add the spacer to the list item row

  1. At the end of the Row block in the DogItem() composable function, add a Spacer. Pass in a Modifier with weight(1f). The Modifier.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 = { }
   )
}
  1. Build & Refresh the preview in the Design pane. Notice that the expand more button is now aligned to the end of the list item.

f6a140413de9ad54.png

5. Add Composable to display hobby

In this task, you'll add Text composables to display the dog's hobby information.

66ea5cc5c7253d55.png

  1. Create a new composable function called DogHobby() that takes in a dog's hobby string resource ID and an optional Modifier.
  2. 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
       )
   ) { }
}
  1. 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.

3051387c4b9c7455.png

  1. For the About text, set the style as h3 (Heading 3) and the color as onBackground. For the hobby information, set the style to body1.
Column(
   modifier = modifier.padding(
       //..
   )
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.h3,
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.body1,
   )
}
  1. 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
       )
   }
}
  1. To display the DogHobby() composable, in DogItem(), wrap the Row with a Column. Make a call to the DogHobby() function, passing in the dog.hobbies as parameter, after the Row 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(
                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)
       }
   }
}
  1. Build & Refresh the preview in the Design pane. Notice the dog's hobby displays.

9e2e68a4bc4a8ae1.png

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.

  1. In the DogItem() composable function, in the DogItemButton() function call, define the onClick() lambda expression, change the expanded boolean state value to true when the button is clicked, and change it back to false if the button is clicked again.
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. At the DogItemButton() function, wrap the DogHobby() function call with an if check on the expanded 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.

  1. 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 42379dbe94a7a497.png in the top-right corner of the Design pane. This starts the preview in interactive mode.

2a4ad1f3d2d0bff7.png

  1. 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.

6ee6774b5b14c7e1.gif

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 c761ef298c2aea5a.png, and ExpandLess displays the upward arrow b380f933be0b6ff4.png.

  1. In the DogItemButton() function, update the imageVector value based on the expanded 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,
           //..
       )
   }
}
  1. Run the app on a device or emulator, or use the interactive mode in the preview again. Notice the icon alternates between the ExpandMore c761ef298c2aea5a.png and the ExpandLess b380f933be0b6ff4.png icons.

bf8bb280a774a6d4.gif

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.

7b52f63dc639c28d.gif

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.

spring effectHigh Bounce

spring effectNo Bounce

High Stiffness

Very low Stiffness

Now, you will add a spring animation to the app!

  1. In MainActivity.kt, in DogItem(), add a modifier parameter to the Column 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)
           }
}
  1. 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.

  1. Add the animationSpec parameter to the animateContentSize() function call. Set it to a spring animation with DampingRatioMediumBouncy and StiffnessLow 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
           )
       )
)
  1. 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.

8cf711b8821b4696.gif

Rerun the app on your emulator or device and enjoy your beautiful app with animations!

1e9cf1dbc490924a.gif

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:

  1. Declare a color and delegate its initialization to animateColorAsState() function.
  2. Set the targetValue named parameter, depending on the expanded boolean value.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   val color by animateColorAsState(
       targetValue = if (expanded) Green25 else MaterialTheme.colors.surface,
   )
   Card(
       //..
   ) {...}
}
  1. Set the color you declared above as the background modifier to the Column.
@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!

Learn more