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. By Andres Torres.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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