Jetpack Compose Animations

Updated: Dec 12, 2021

Animations are fundamental for mobile applications. It gives smooth user experience to end users. Jetpack Compose has various animation API's. Choose your suitable API based on your requirements.


In this tutorial we will learn about following animation APIs,

  1. Animatable

  2. Animate*AsState - AnimateDpAsState - AnimateColorAsState - AnimateFloatAsState

  3. UpdateTransition

  4. InfiniteTransition


1.Animatable

Animatable is a coroutine-based API for animating a single value. We can animate color or float values using this API. It's different from all other animation APIs, because you can use this API outside of your composable function.


Example:

@Composable
private fun AnimatableSample() {
    var isAnimated by remember { mutableStateOf(false) }

    val color = remember { Animatable(Color.DarkGray) }

    // animate to green/red based on `button click`
    LaunchedEffect(isAnimated) {
        color.animateTo(if (isAnimated) Color.Green else Color.Red, animationSpec = tween(2000))
    }

    Box(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(0.8f)
            .background(color.value)
    )
    Button(
        onClick = { isAnimated = !isAnimated },
        modifier = Modifier.padding(top = 10.dp)
    ) {
        Text(text = "Animate Color")
    }
}

Explanation:

In LaunchEffect() we give a condition to change the color based on 'isAnimated' value. When you press the button, 'isAnimated' value gets changed and LaunchEffect() will trigger to change the color.

LaunchedEffect should be used when you want that some action must be taken when composable is first launched

Output:

In the above example, we switch the one color to another. But we need to give the some interval duration for color switching then only we can feel the animation, else it will switch color immediately and you can't notice the animation effects.

These kind of customization achieved by animationSpec.

color.animateTo(if (isAnimated) Color.Green else Color.Red, animationSpec = tween(2000))

Following functions are available to customize your animations:

  1. tween

  2. keyframes

  3. spring

  4. repeatable

  5. infiniteRepeatable

  6. snap

For more details about animationSpec: https://developer.android.com/jetpack/compose/animation#animationspec


2. Animate*AsState

It's used for animating a single value. It can be Dp, Color, Float, Integer, Offset, Rect, Size.

Following functions are available in animate*AsState.


2a. AnimateDpAsState

It returns a State object. The value of the state object will continuously be updated by the animation until the animation finishes.

Note: We can't stop/cancel the animation at runtime in AnimateDpAsState until we remove it from the tree.

@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    finishedListener: ((Dp) -> Unit)? = null
)

We will try to change the image size at runtime using animateDpAsState.


Step 1: For code simplify we create a separate function for circle image.

@Composable
fun CircleImage(imageSize: Dp) {
    Image(
        painter = painterResource(R.drawable.andy_rubin),
        contentDescription = "Circle Image",
        contentScale = ContentScale.Crop,  
        modifier = Modifier
            .size(imageSize)
            .clip(CircleShape) 
            .border(5.dp, Color.Gray, CircleShape) 
    )
}

Step 2: AnimateDpAsState sample code:

@Composable
private fun AnimateDpAsState() {
    val isNeedExpansion = rememberSaveable{ mutableStateOf(false) }
    
    val animatedSizeDp: Dp by animateDpAsState(targetValue = if (isNeedExpansion.value) 350.dp else 100.dp)

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        CircleImage(animatedSizeDp)
        Button(
            onClick = { 
            isNeedExpansion.value = !isNeedExpansion.value 
            },
            modifier = Modifier
                .padding(top = 50.dp)
                .width(300.dp)
        ) {
            Text(text = "animateDpAsState")
        }
    }
}

We change the targetValue based on 'isNeedExpansion'. We toggle the 'isNeedExpansion' when button clicked.


Output:


2b. AnimateColorAsState

It exactly same as animateDpAsState(). But the only difference is we give the color as the target value.

@Composable
fun animateColorAsState(
    targetValue: Color,
    animationSpec: AnimationSpec<Color> = colorDefaultSpring,
    finishedListener: ((Color) -> Unit)? = null
)

Example:

@Composable
private fun AnimateColorAsState() {
    var isNeedColorChange by remember { mutableStateOf(false) }
    val startColor = Color.Blue
    val endColor = Color.Green
    val backgroundColor by animateColorAsState(
        if (isNeedColorChange) endColor else startColor,
        animationSpec = tween(
            durationMillis = 2000,
            delayMillis = 100,
            easing = LinearEasing
        )
    )
    Column {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight(0.8f)
                .background(backgroundColor)
        )
        Button(
            onClick = { isNeedColorChange = !isNeedColorChange },
            modifier = Modifier.padding(top = 10.dp)
        ) {
            Text(text = "Switch Color")
        }
    }
}

Output:


2c. AnimateFloatAsState

It's also same as animateDpAsState(). But the only difference is we give the float value as the target value.

@Composable
fun animateFloatAsState(
    targetValue: Float,
    animationSpec: AnimationSpec<Float> = defaultAnimation,
    visibilityThreshold: Float = 0.01f,
    finishedListener: ((Float) -> Unit)? = null
)

visibilityThreshold - An optional threshold for deciding when the animation value is considered close enough to the targetValue.


Example:

@Composable
private fun AnimateAsFloatContent() {
    var isRotated by rememberSaveable { mutableStateOf(false) }
    val rotationAngle by animateFloatAsState(
        targetValue = if (isRotated) 360F else 0f,
        animationSpec = tween(durationMillis = 2500)
    )
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Image(
            painter = painterResource(R.drawable.fan),
            contentDescription = "fan",
            modifier = Modifier
                .rotate(rotationAngle)
                .size(150.dp)
        )

        Button(
            onClick = { isRotated = !isRotated },
            modifier = Modifier
                .padding(top = 50.dp)
                .width(200.dp)
        ) {
            Text(text = "Rotate Fan")
        }
    }
}

In this example, we rotate the image angle from 0 to 360 usingfrom 0 to 360 using animateFloatAsState().


Output:

3. UpdateTransition

All the previous animations can run one animation at a time. But updateTransition() can animate one or multiple animations simultaneously.


In the following example, we will try to move the object position + scale the object with the help of updateTransition()


Step 1: Create a state object to trigger the animation state

var isAnimated by remember { mutableStateOf(false) }

Step 2: Define the updateTransition animation

val transition = updateTransition(targetState = isAnimated, label = "transition")

Step 3: Animation1 - Animate the offset (position)

val rocketOffset by transition.animateOffset(transitionSpec = {
    if (this.targetState) {
        tween(1000) // launch duration

    } else {
       tween(1500) // land duration
    }
}, label = "rocket offset") { animated ->
    if (animated) 
        Offset(200f, 0f) 
    else Offset(200f, 500f)
}

Step 4: Animation2 - Animate the size


val rocketSize by transition.animateDp(transitionSpec = {
    tween(1000)
}, "") { animated ->
    if (animated) 75.dp else 150.dp
}

Step 5: Use the Animation1 & Animation2 in your composable(view)

Image(
    painter = painterResource(id = R.drawable.rocket),
    contentDescription = "Rocket",
    modifier = Modifier
        .size(rocketSize)
        .alpha(1.0f)
        .offset(rocketOffset.x.dp, rocketOffset.y.dp)
)

Step 6: Update the animation state to trigger your animation

Button(
    onClick = { isAnimated = !isAnimated }
) {
    Text(text = if (isAnimated) "Land rocket" else "Launch rocket")
}

Full code:

@Composable
private fun TransitionAnimation() {
    var isAnimated by remember { mutableStateOf(false) }
    val transition = updateTransition(targetState = isAnimated, label = "transition")

    val rocketOffset by transition.animateOffset(transitionSpec = {
        if (this.targetState) {
            tween(1000) // launch duration

        } else {
           tween(1500) // land duration
        }
    }, label = "rocket offset") { animated ->
        if (animated) Offset(200f, 0f) else Offset(200f, 500f)
    }

    val rocketSize by transition.animateDp(transitionSpec = {
        tween(1000)
    }, "") { animated ->
        if (animated) 75.dp else 150.dp
    }


    Column(
        modifier = Modifier
            .fillMaxSize()
    ) {
        Image(
            painter = painterResource(id = R.drawable.rocket),
            contentDescription = "Rocket",
            modifier = Modifier
                .size(rocketSize)
                .alpha(1.0f)
                .offset(rocketOffset.x.dp, rocketOffset.y.dp)
        )
        Button(
            onClick = { isAnimated = !isAnimated },
            modifier = Modifier.padding(top = 10.dp)
        ) {
            Text(text = if (isAnimated) "Land rocket" else "Launch rocket")
        }
    }
}

Output



4. InfiniteTransition

It creates a InfiniteTransition that runs infinite child animations. Child animations can be added using InfiniteTransition.animateColor, InfiniteTransition.animateFloat, or InfiniteTransition.animateValue. Child animations will start running as soon as they enter the composition, and will not stop until they are removed from the composition.


We will try InfiniteTransition.animateFloat to animate the heart.

@Composable
fun InfiniteAnimation() {
    val infiniteTransition = rememberInfiniteTransition()

    val heartSize by infiniteTransition.animateFloat(
        initialValue = 100.0f,
        targetValue = 250.0f,
        animationSpec = infiniteRepeatable(
            animation = tween(800, delayMillis = 100,easing = FastOutLinearInEasing),
            repeatMode = RepeatMode.Reverse
        )
    )
    Image(
        painter = painterResource(R.drawable.heart),

        contentDescription = "heart",
        modifier = Modifier
            .size(heartSize.dp)
    )
}

we give the initialValue =100 and targetValue = 250. So it will animate from 100 to 250 values.


RepeatMode:

The animation could either restart after each iteration (i.e. RepeatMode.Restart), or reverse after each iteration (i.e . RepeatMode.Reverse).


Output:


For more details refer the official documentation:

https://developer.android.com/jetpack/compose/animation#animate-as-state


Source code:


https://github.com/JetpackCompose/Jetpack-Compose-Samples/blob/master/JetPackComposeSamples/app/src/main/java/net/jetpackcompose/composetext/activities/ActivityAnimations.kt


8,919 views0 comments

Recent Posts

See All

This site developed for Jetpack Compose tutorial. We will post tutorials frequently. You can also join our community by clicking login. 

  • Twitter
  • LinkedIn

You have any queries contact me. 

Subscribe for latest updates

Thanks for submitting!