Animation in Jetpack Compose | Animatable with Examples

Jetpack Compose Animation

Jetpack Compose has several animation APIs. Animatable is one of them. In this article, we will learn what it is and how to use it.

Prerequisites:

First, create an empty Compose project and open MainActivity.kt. Create a MyUI() composable and call it from the onCreate() method. We’ll write our code in it.

MainActivity for the Material 3 Jetpack Compose:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.repeatable
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YourProjectNameTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column(
                        modifier = Modifier.fillMaxSize(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        MyUI()
                    }
                }
            }
        }
    }
}

@Composable
fun MyUI() {

}

MainActivity for the Material 2 version:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.repeatable
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YourProjectNameTheme(darkTheme = false) {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Column(
                        modifier = Modifier.fillMaxSize(),
                        horizontalAlignment = Alignment.CenterHorizontally,
                        verticalArrangement = Arrangement.Center
                    ) {
                        MyUI()
                    }
                }
            }
        }
    }
}

@Composable
fun MyUI() {

}

Let’s create a red color box with 40 dp size.

@Composable
fun MyUI() {
    Box(
        modifier = Modifier
            .size(size = 40.dp)
            .background(color = Color.Red)
    )
}

We will animate the size of the Box() using Animatable. We can initialize Animatable using the remember composable.

val scale = remember {
    Animatable(initialValue = 1f)
}

Add clickable and scale modifiers to the Box() so that when we tap on it, the size will be changed using the scale property.

@Composable
fun BoxSizeAnimation() {
    
    val scale = remember {
        Animatable(initialValue = 1f)
    }

    Box(
        modifier = Modifier
            .scale(scale = scale.value) // scale
            .size(size = 40.dp)
            .background(color = Color.Red)
            .clickable {
            // we'll add the code here

            }
    )
}

Note: Add scale() at the beginning because the order of modifiers matter.

Animatable animates the value that is changed via animateTo function. It is a suspend function. So, we need a coroutine scope. We can get the scope by using rememberCoroutineScope().

val coroutineScope = rememberCoroutineScope()

Let’s make the box 4 times bigger. Inside the clickable block, launch the coroutine and call the animateTo() with the targetValue = 4f.

@Composable
fun MyUI() {
    val coroutineScope = rememberCoroutineScope()

    val scale = remember {
        Animatable(initialValue = 1f)
    }

    Box(
        modifier = Modifier
            .scale(scale = scale.value)
            .size(size = 40.dp)
            .background(color = Color.Red)
            .clickable {
                coroutineScope.launch {
                    scale.animateTo(targetValue = 4f)
                }
            }
    )
}

Output:

Jetpack Compose Animatable

The animateTo() function has animationSpec parameter. It allows us to customize the animation.

The following are the different kinds of AnimationSpec APIs.

  • tween
  • spring
  • keyframes
  • repeatable
  • infiniteRepeatable
  • snap

tween:

It accepts 3 parameters – durationMillis, delayMillis, and easing.

durationMillis – The duration of the animation in milliseconds.

delayMillis – You can postpone (or delay) the animation.

easing – It allows us to change the animation’s rate of change. We can speed up and slow down the animation value, rather than moving at a constant rate.

Here is an example:

scale.animateTo(
    targetValue = 4f,
    animationSpec = tween(
        durationMillis = 2000,
        delayMillis = 1000,
        easing = FastOutLinearInEasing
    )
)

Output:

tween animation

When you tap on the box, the animation will be started after 1 second and continued for 2 seconds. FastOutLinearInEasing starts the animation slowly and ends quickly.

Jetpack Compose provides several Easing functions out of the box. They are:

  • FastOutLinearInEasing
  • FastOutSlowInEasing
  • LinearOutSlowInEasing
  • FastOutLinearEasing
  • LinearEasing
  • CubicBezierEasing

You can learn more about them here: material.io/design/motion/speed.html#easing

spring:

It creates a physics-based animation. It takes 2 parameters: dampingRatio and stiffness.

dampingRatio – It decides the bouncing of the animation.

stiffness – It decides how fast the spring should move towards the end value.

scale.animateTo(
    targetValue = 4f,
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy,
        stiffness = Spring.StiffnessHigh
    )
)

Output:

spring animation spec

The animation is completed quickly because we have set DampingRatioHighBouncy and StiffnessHigh.

Here are the values that Jetpack Compose provides out of the box:

dampingRatio:

  • Spring.DampingRatioNoBouncy
  • Spring.DampingRatioLowBouncy
  • Spring.DampingRatioMediumBouncy
  • Spring.DampingRationHighBouncy

stiffness:

  • Spring.StiffnessHigh
  • Spring.StiffnessMedium
  • Spring.StiffnessMediumLow
  • Spring.StiffnessLow
  • Spring.StiffnessVeryLow

Learn more about spring here: developer.android.com/jetpack/compose/animation#spring

keyframes:

We can include keyframes. We can also specify Easing for each keyframe.

scale.animateTo(
    targetValue = 4f,
    animationSpec = keyframes {
        durationMillis = 4000
        1f at 1000 with LinearOutSlowInEasing // 0 - 1000 ms
        2f at 1100 with FastOutLinearInEasing // 1000 - 1100 ms
        3f at 1200 // 1100 - 1200 ms
        4f at 4000 // 1200 - 4000 ms
    }
)

Output:

keyframes jetpack compose

In the above code, we have set the overall duration to 4 seconds (durationMillis = 4000). Our initial value of the scale is 1f. We have added 4 keyframes.

1f at 1000 with LinearOutSlowInEasing:

From 0 to 1000 ms, the scale value is increased from the initial value to 1f. Since the initial value is also 1f, we haven’t seen any animation during the first 1000 ms.

2f at 1100 with FastOutLinearInEasing:

From 1000 to 1100 ms, the scale value is increased from 1f to 2f (with FastOutLinearInEasing).

3f at 1200:

From 1100 to 1200 ms, the scale value is increased from 2f to 3f.

4f at 4000:

From 1200 to 4000 ms, the scale value is increased from 3f to 4f.

Note: You don’t have to mention the values at 0 ms and at the duration time. If you don’t specify these values, they default to the start and end values of the animation, respectively.

repeatable:

We can repeat the animation using repeatable(). It takes 3 parameters: iterations, animation, and repeatMode.

iterations – How many times the animation should be repeated (it is an integer value).

animation – It is the animation that should be repeated. You can specify duration-based animations (such as tween or keyframes)

repeatMode – Whether the animation should repeat by starting from the beginning (RepeatMode.Restart) or from the end (RepeatMode.Reverse).

scale.animateTo(
    targetValue = 4f,
    animationSpec = repeatable(
        iterations = 4,
        animation = tween(durationMillis = 1000),
        repeatMode = RepeatMode.Reverse
    )
)

Output:

repeatable

infiniteRepeatable:

It is the same as the repeatable() but it animates forever.

scale.animateTo(
    targetValue = 4f,
    animationSpec = infiniteRepeatable(
        animation = tween(durationMillis = 1000),
        repeatMode = RepeatMode.Reverse
    )
)

Output:

repeat forever infinite

Note: To stop the animation call scale.stop() method.

snap:

It changes the value to the end value immediately. You can specify the delay for the animation using delayMillis.

scale.animateTo(
    targetValue = 4f,
    animationSpec = snap(delayMillis = 1000)
)

Output:

snap

One of the best things about Animatable is that we can run multiple animations one after the other or at the same time.

Let us animate the color of the box after the size animation is finished. First, declare a color variable.

val color = remember {
    Animatable(initialValue = Color.Red)
}

Set this color in the Modifer.background() function.

Box(
    modifier = Modifier
        .scale(scale = scale.value)
        .size(size = 40.dp)
        .background(color = color.value) // add background color
        .clickable {
        }
)

In the clickable block, launch a coroutine and call scale.animateTo() and color.animateTo() methods one after the other.

coroutineScope.launch {
    scale.animateTo(
        targetValue = 4f,
        animationSpec = tween(durationMillis = 2000)
    )

    color.animateTo(
        targetValue = Color.Yellow,
        animationSpec = tween(durationMillis = 2000)
    )
}

Output:

Jetpack compose animation one after another 2

What if you want to animate both the size and the color simultaneously? Just put the animateTo() functions in separate launch blocks.

coroutineScope.launch {
    launch {
        scale.animateTo(
            targetValue = 4f,
            animationSpec = tween(durationMillis = 2000)
        )
    }

    launch {
        color.animateTo(
            targetValue = Color.Yellow,
            animationSpec = tween(durationMillis = 2000)
        )
    }
}

Output:

parallel animation

This is all about Animatable API. I hope you have learned something new about the animations in Jetpack Compose. If you have any doubts, comment below.

Related Articles:


References:

6 thoughts on “Animation in Jetpack Compose | Animatable with Examples”

Leave a Comment