Home · Android & Kotlin Tutorials

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.

5/5 5 Ratings

Version

  • Kotlin 1.4, Android 10.0, Android Studio 4.2

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!

Note: This tutorial assumes you’re familiar with the basics of Jetpack Compose. If you’re new to this, check out this Jetpack Compose tutorial and this more in-depth Jetpack Compose Primer tutorial.

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:

Favenimate starter 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.

Diagram of the states to be animated with Jetpack Compose

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.

Jetpack Compose Preview tooling

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.

Note: You can use the preview and interactive modes throughout the tutorial, or simply build the app on an emulator, whenever the instructions say to build and run the app. Both options are fine, and you can choose which suits you the best! :]

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:

  1. As mentioned before, @Preview will allow you to visualize this component in Android Studio, without having to build the entire app.
  2. @Composable tells the compiler that the function is a composable function, which will serve as the reusable component for your animated button.
  3. 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 PropKeys. A PropKey refers to a property and an animation vector you want to animate. You can easily create your own custom PropKeys but Jetpack Compose Animations come with several built-in PropKeys 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:

  1. You create a variable for the transitionDefinition of your component.
  2. Next, you declare an initial state where you tell the transitionDefinition that it will hold your previously defined width DpPropKey with a value of 300 dp for the IDLE state.
  3. 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.
  4. 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 using tween() and that the whole animation will take 1,500 milliseconds. tween() component extends DurationBasedAnimationBuilder, which in turn extends AnimationBuilder and builds the tween animation from a start and end value, based on an easing curve and a duration. Other types of AnimationBuilders you can work with to recreate different animations are PhysicsBuilder, RepeatableBuilder, SnapBuilder and KeyframesBuilder. You’ll work with some of them later in this tutorial.
  5. Finally, you build the animation using transition(). Then you use the return value of the transition as a TransitionState, and pass it, and the buttonState to the FavButton.

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.

Note: It’s worth noting that even though they have the same name, the 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! :]

Button animating its width property

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!

Button reverting animation

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!

Button animating rounded corners

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!

Button animating text and background color

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:

  1. Based on the state of the button, you switch the component that works as the content of the button, either a Row or an Icon.
  2. You add a modifier to the text that will slowly fade it in/out based on the button state.
  3. You add a full heart icon that will appear when the button is in pressed state.
  4. 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.

Button animating opacity for text and icon

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. :]

Button animating the icon for the pressed state

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:

  1. 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.
  2. You describe the animation that will be repeated, using keyframes.
  3. 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!

Button animating beating heart

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!

Average Rating

5/5

Add a rating for this content

5 ratings

More like this

Contributors

Comments