DEV Community

Cover image for State Driven Animations in Jetpack Compose
Audu Ephraim
Audu Ephraim

Posted on

State Driven Animations in Jetpack Compose

Animation in the context of an Android app refers to adding visual effects to different components and elements to make it more interactive and appealing. It involves making components and elements move from one place to another, change shape, transition from one state to another, etc..

State-driven animations in Jetpack Compose allow to you create dynamic and interactive user Interfaces with smooth transitions.

By using state variables to control animations that help you create a responsive and engaging user experience that is visually appealing.

With Jetpack Compose’s declarative syntax, it is easy to define animations that respond to state changes. Hence making it easier to write complex animations.

In this blog post, we will explore how to create and implement custom state-driven animations in Jetpack Compose, allowing you to add a touch of creativity to your Android applications.

Prerequisites

To follow along easily with the article, you need to know the following:

Familiarity with Kotlin
Understanding of basic compose concepts
How to set up of Android development environment
Knowledge of state management in Jetpack Compose.

Let’s get started

To continue, we need to launch Android Studio, which is the official IDE for building Android applications, if you do not have that installed you can download the latest version here and follow the prompts to complete the installation.

Create a new project(an empty compose activity), and give it your chosen name.
Leave all other options as they are, or if you want your application to run only on newer Android devices you can select a minimum SDK, then click finish.

Allow your project to build and download all the necessary dependencies until all dependencies have been downloaded and synced correctly.

In your MainActivity.kt file delete the default greeting composable and add a new composable RotationDemo:
Next, edit the onCreate method and GreetingPreview to call RotationDemo, and finally edit GreetingPreview to RotationDemoPreview

Animate as state functions

In compose there are special functions called the Animate*AsState functions that help with the animations. The “*” is replaced by the type of thing you are animating. So if you are changing the background color and want to animate it you use “AnimateColorAsState”.

Compose has these functions for different things like Rect, Offset, IntOffset, Int, Float, Dp, Color, Bounds

Animating Rotation with animateFloatAsState

In this first example, we are going to animate the rotation of a text component by the click of a button. Since in Compose the angle of rotation is represented as a float value. So we’ll use a function called “animateFloatAsState” to create this animation.

Let’s create the component we will be animating. In your main activity, modify your rotationDemo function to design the user interface layout like so:

@Composable
fun RotationDemo() {
   var rotated by remember { mutableStateOf(false) }
   Column(
       horizontalAlignment = Alignment.CenterHorizontally,
       modifier = Modifier.fillMaxWidth()
           .padding(vertical = 40.dp)
   ) {

       Text(
           text = "this is a rotation animation trial",
       )

       Button(
           onClick = { rotated = !rotated },
           modifier = Modifier.padding(10.dp)
       ) {
           Text(text = "rotate text")
       }

   }

}
Enter fullscreen mode Exit fullscreen mode

Head over to the preview pane, refresh your build and you should see something like this:

Image description

The layout consists of a column that contains a text component that displays “this is an animation trial” and a button.

In the code, there is also a boolean state variable named “rotate” which changes each time the button is clicked, although this button changes the state value but is not yet connected to an animation therefore we need to now add the the animateFloatAsState function by adding the following code:

val angle by animateFloatAsState(
   targetValue = if (rotated) 360f else 0f,
   animationSpec = tween(durationMillis = 2500, easing = LinearOutSlowInEasing), label = "")
Enter fullscreen mode Exit fullscreen mode

Next, edit the Text declaration and pass the angle state through the rotate() modifier

Text(
   text = "this is a rotation animation trial",
   modifier = Modifier
       .rotate(angle)
)
Enter fullscreen mode Exit fullscreen mode

This code uses animateAsFloat() and assigns the result to a variable called angle. If ‘rotated’ is set to true, then ‘angle is set to 360 degrees. If not the angle is set to 0 degrees meaning no rotation.

Now time to test. Using either an emulator or a physical device. Call the rotationDemo function in your mainActivity and run the application. Click on the button, the text should rotate 360 degrees in a clockwise direction. A second click will rotate the text back to 0 degrees.

The rotation is currently using the default FastOutSlowEasing easing setting where the animation rate slows as the propeller nears the end of the rotation. To see other easing options add them to the tween call. The LinearOutSlowInEasing call animates the rotation at a constant speed.

You can also increase and decrease the value of durationMillis to increase and decrease the speed/duration of the animation. You can also use different easing calls like FastOutLinearEasing, EaseEasing, EaseOutEasing, and CubicBezierEasing(which allows you to create custom functions by specifying your control points) amongst others.

Animating Color Changes with animateColorAsState

In this example we will be animating, the change between two colors as it changes from one color to another. In this layout, we will have a box and a button. Also, we will have an enum class to hold the two background color options.

First of all, create the enum class:
enum class ColorToChange{
Red, Magenta
}

Create a new composable function colorAnimation

@Composable
fun ColorAnimation() {

   var colorState by remember{ mutableStateOf(ColorToChange.Red) }

   Column(
       horizontalAlignment = Alignment.CenterHorizontally,
       modifier = Modifier.fillMaxWidth()
   ) {
       Box(
           modifier = Modifier
               .padding(20.dp)
               .size(200.dp)
               .background(Color.Red)
       )
       Button(
           onClick = {
               colorState = when(colorState){
                   ColorToChange.Red -> ColorToChange.Magenta
                   ColorToChange.Magenta->ColorToChange.Red
               } },
           modifier = Modifier.padding(10.dp)
       ) {
           Text(text = "change color")

       }
   }
}
Enter fullscreen mode Exit fullscreen mode

The BoxBackground enum class contains two possible colors red and magenta. A state variable called colorState is initialized to BoxBackgroud.Red.
Next, the button click listener uses a when statement to switch the colorState value between the red and magenta BackgroundColor enumeration values. Call the colorAnimation in your preview. It should look something like this:

Image description

Now all that is left is to use the animateColorAsState() function to implement and animate the color change of the background. Add the following to your ColorAnimation composable:

val animatedColor: Color by animateColorAsState(
   targetValue = when (colorState) {
       ColorToChange.Red -> Color.Magenta
       ColorToChange.Magenta -> Color.Red
   },
   animationSpec = tween(4500), label = "")
Enter fullscreen mode Exit fullscreen mode

Modify the background color of the box modifier to use animatedColor:

Box(
    modifier = Modifier
        .padding(20.dp)
        .size(200.dp)
        .background(animatedColor)
)
Enter fullscreen mode Exit fullscreen mode

The code used the current colorState color value to set the animation target to the other color. This starts the animation which happens over a 4500 millisecond duration. You can play around with this figure as you like, you can also add the easing parameter to the animationSpec and choose among any of the easing functions.

Call the colorAnimation composable in mainactivity, at the click of the button, the color changes from red to magenta, at a second click it changes to back red while being animated

Animating Motion with animateDpAsState

In this final example, we will be animating the movement of a box from one part of our device screen to another part. This will involve changing the x-position offset of a box as it moves to a new location on the screen. As we've been doing in our previous examples.

Create a new composable movementAnimation:

fun MovementAnimation() {
   var boxState by remember{ mutableStateOf(BoxPosition.Start) }
   val boxSideLength = 70.dp

   Column(
       modifier = Modifier.fillMaxWidth()
   ) {
       Box (
           modifier = Modifier
               .size(boxSideLength)
               .background(Color.Red)
       )

       Spacer(modifier = Modifier.height(50.dp))

       Button(
           onClick = {
               boxState = when(boxState){
                   BoxPosition.Start -> BoxPosition.End
                   BoxPosition.End -> BoxPosition.Start
               }
           },
           modifier = Modifier
               .padding(20.dp)
               .align(Alignment.CenterHorizontally)
       ) {
           Text(text = "Move box")

       }
   }

}
Enter fullscreen mode Exit fullscreen mode

As with the color change example, we also need an enum class to contain the different positions the component can move to. Call the enum class BoxPosition:

enum class BoxPosition{
   Start,
   End
}
Enter fullscreen mode Exit fullscreen mode

This example is similar to the color change example, and it is structured the same way, except that this time we are working with density-independent pixels and not colors. The goal is to animate the box movement from one side of the screen to the other along the x-axis (horizontally).

If you preview this, you should see something like this:

Image description

Assuming we will need to use this application on a variety of devices with different screen sizes, we need to know the width of the screen. We can get this information by accessing the properties of the local configuration instance.

This is an object accessible to every composed-based app, that provides access to properties like screen height, screen width, screen density, and whether or not night mode is currently turned on on the device amongst others.

In our case, we need only the width which can be obtained as follows:

val screenWidth = (LocalConfiguration.current.screenWidthDp.dp)
Enter fullscreen mode Exit fullscreen mode

Following that, we need to add the animation using the animateDpAsState() function:

val animateMotion: Dp by animateDpAsState(
   targetValue = when(boxState){
       BoxPosition.Start -> 0.dp
       BoxPosition.End -> screenWidth - boxSideLength
   },animationSpec = tween(500), label = ""
)
Enter fullscreen mode Exit fullscreen mode

In this, there is a target spot that can be either at the beginning or at the end of the screen, depending on what box state is set to. If the target is at the end we subtract the box width from the screen width.

This way the box does not move out of the screen when it reaches the end of the screen. Now that we have animatedOffset declared, we can pass it as the x parameter to the box offset() modifier:

Box (
   modifier = Modifier
       .offset(animateMotion, y = 20.dp)
       .size(boxSideLength)
       .background(Color.Red)
)
Enter fullscreen mode Exit fullscreen mode

Call MovementAnimation in mainActivity and run the application. At the click of the button you see the box, move from one side of the screen to the other, at the second click of the button, it returns to the initial position.

Adding Spring Effects

The above example provides an opportunity to talk about spring effects. This effect adds a bouncy effect to animations and is applied using the spring function via the animation spec parameter.

The ‘spring()’ function has two important parts: the damping ratio and stiffness. The damping ratio is about how quickly the bounce slows down. It’s a decimal number where 1.0 means no bounce and 0.1 means a lot of bounce. You can also use some ready-made options instead of decimal numbers when setting the damping ratio:

DampingRatioHighBouncy
DampingRatioLowBouncy
DampingRationMediumBouncy
DampingRatioNoBouncy

To add a spring effect to the motion animation, add a spring function call to the existing code as follows:

val animateMotion: Dp by animateDpAsState(
   targetValue = when(boxState){
       BoxPosition.Start -> 0.dp
       BoxPosition.End -> screenWidth - boxSideLength
   },
   animationSpec = spring(DampingRatioHighBouncy, stiffness = StiffnessMediumLow), label = ""
)
Enter fullscreen mode Exit fullscreen mode

When tested, the box will bounce when it reaches the target destination.

The stiffness parameter is about how strong the spring is. If you use a softer spring (lower stiffness), the box will bounce more. For example, if you mix a high bounce with a soft spring, you get a super bouncy animation. The box will even bounce off the screen a few times before it settles at the end.

The stiffness of the spring effect can be adjusted using the following constants:
StiffnessHigh
StiffnessLow
StiffnessMedium
StiffnessMediumLow
StiffnessVeryLow

Take some time to experiment with the different damping and stiffness settings to learn more about the effects they produce

Conclusion

Animations can truly transform your applications, adding a dynamic and interactive element that can greatly enhance user experience. This blog post aimed to shed light on the vast potential of animations in Jetpack Compose.

By animating components, you can create a more engaging and visually appealing interface. It’s an exciting aspect of mobile app development, and I hope this blog post has sparked your interest and provided valuable insights. Remember, the possibilities with animations in Jetpack Compose are only limited by your imagination.

Good luck!

In case of any suggestions or issues, you can always reach out!

Top comments (0)