Hey there! Welcome to my blog on animating shapes in android with jetpack compose. I will demonstrate on animation with launched effect, animate color changes, animate float between initial and target values, shapes, repeatable, infinite and finite, remember infinite transition..

You can take reference of the source code from github repository.

This is the preview of what we will build. Let’s get started.

Dependency

Right now I’m using compose version 1.0.5 which is latest release.

implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"

1. Compose elements for Animatable

We need elements to make them animatable.
Add a Box which holds ImageView inside it.

Box(
    modifier = Modifier
        .size(80.dp)
        .clip(CircleShape)
        .background(color = Color(0xFF302522))
        .border(
            width = 2.dp,
            color = Color(0xFFede0dc),
            shape = CircleShape
        )
) {
    Image(
        painter = painterResource(id = R.drawable.ic_compose_image),
        contentDescription = "Compose image",
        modifier = Modifier
            .size(56.dp)
            .align(alignment = Alignment.Center)
    )
}

This results with this preview.

Box

  1. Box clipped with CircleShape.
  2. Box background color which differs with border color.
  3. Box shape and border shape should be same.

Image

  1. Image should be aligned center for better reveal effect.
  2. Whatever elements you use inside the box, make it’s size significantly half the box.

2. Animate shape changes

We will animate the state of float value for border width from initial value to target value for reveal effect.

We need to remember the float initial value of the border.

val animateShape = remember { Animatable(28f) }

Note: Higher the initial value, more width will be animated. In your case prefer what’s best and fits your purpose. Keeping initial value higher only makes the end result to look bad and inappropriate for user experience.

Launch a coroutine in composable which animates our shape with LaunchedEffect.

LaunchedEffect(animateShape) {
    animateShape.animateTo(
        targetValue = 2f,
        animationSpec = repeatable(
            animation = tween(
                durationMillis = 2000,
                easing = LinearEasing,
                delayMillis = 500
            ),
            repeatMode = RepeatMode.Restart,
            iterations = 3
        )
    )
}
  1. animateTo Starts an animation to animate from value to the provided targetValue.
  2. targetValue Is set to 2f, where has initial value is already set to 28f.
  3. repeatable Creates a duration based animation which isn’t default repeatable.
  4. iterations Number of times same animation to repeat.
  5. durationMillis Delay the animation to start.
  6. easing Curve that will be used to interpolate between start and end.
  7. repeatMode Point of animation to start from beginning or from the end.

Now we have all animation properties, let’s apply it to the elements.

Replace the width of the box border from 2.dp to initial float value. By doing this, will set the initial float state of border and with animation starts it animates the changes to target float value.

Box(
    Modifier.border(
        width = Dp(animateShape.value)
    )
) {
    Image()
}

At this point, we are able to achieve this result.

These are final changes in one composable function from above parts.

@Composable
fun RevealAnimation() {
    val animateShape = remember { Animatable(28f) }
    LaunchedEffect(animateShape) {
        animateShape.animateTo(
            targetValue = 2f,
            animationSpec = repeatable(
                animation = tween(
                    durationMillis = 2000,
                    easing = LinearEasing,
                    delayMillis = 500
                ),
                repeatMode = RepeatMode.Restart,
                iterations = 3
            )
        )
    }

    Box(
        modifier = Modifier
            .size(80.dp)
            .clip(CircleShape)
            .background(color = Color(0xFF302522))
            .border(
                width = Dp(animateShape.value),
                color = Color(0xFFede0dc),
                shape = CircleShape
            )
    ) {
        Image(
            painter = painterResource(id = R.drawable.ic_compose_image),
            contentDescription = "Compose image",
            modifier = Modifier.size(56.dp).align(alignment = Alignment.Center)
        )
    }
}

3. Animate color changes

While box border animation lasts for 2 seconds from initial to target value, we can also animate their color changes simultaneously with same animatable properties.

First declare initial and target colors, just the way we did for shape changes.

val initialColor = Color(0xFF302522)
val targetColor = Color(0xFFede0dc)

Remember the initial color value.

val animateColor = remember { Animatable(initialColor) }

Launch a coroutine in composable which animates color changes with LaunchedEffect.

LaunchedEffect(animateColor) {
    animateColor.animateTo(
        targetValue = targetColor,
        animationSpec = repeatable(
            animation = tween(
                durationMillis = 2000,
                easing = LinearEasing,
                delayMillis = 500
            ),
            repeatMode = RepeatMode.Restart,
            iterations = 3
        )
    )
}

Above, we launch delayed repeatable animation in coroutine which iterates 3 times animating color changes between initial and target values.

Now assign the value of initial color to border color of box. And add the remaining properties similar to previous one.

Box(
    Modifier.border(
        width = Dp(animateShape.value),
        color = animateColor.asState().value
    )
) {
    Image()
}

This doesn’t change shape, but animates its color changes.

4. Infinite repeatable animation

Having an infinitely repeatable animation in this scenario pretty useless. But here’s how you can do one.

LaunchedEffect(animateShape) {
    animateShape.animateTo(
        targetValue = 2f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 2000,
                easing = LinearEasing,
                delayMillis = 500
            ),
            repeatMode = RepeatMode.Restart
        )
    )
}

Property iterations is no more of use since this repeats infinitely itself by calling infiniteRepeatable( ) instead of repeatable.

We can directly use remeberInfinteTransition( ) to run animation, if we not concerned about iterations.
Create a InfiniteTransition that runs infinite child animations. Child animations will start running as soon as they enter the composition, and will not stop until they are removed from the composition.

val infiniteTransition = rememberInfiniteTransition()

With that variable you can animate float value changes.

val animateShape by infiniteTransition.animateFloat(
    initialValue = 28f,
    targetValue = 2f,
    animationSpec = infiniteRepeatable(
        animation = tween(
            durationMillis = 2000,
            easing = LinearEasing
        ),
        repeatMode = RepeatMode.Restart
    )
)

Box(
    Modifier.border(width = Dp(animateShape))
) {
    Image()
}

Similarly you can animate color changes too.

val infiniteTransition = rememberInfiniteTransition()
val animateColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF302522),
    targetValue = Color(0xFFede0dc),
    animationSpec = infiniteRepeatable(
        animation = tween(
            durationMillis = 2000,
            easing = LinearEasing
        ),
        repeatMode = RepeatMode.Restart
    )
)

Box(
    Modifier
        .border(
            color = animateColor,
            width = Dp(animateShape)
        )
) {
    Image()
}

This is will create infinite child animations.

5. Use case in app

Observe the below preview.

  1. All three elements have same animation effect properties.
  2. Two Text elements & one Image are aligned center.
  3. Shape, initial/target states, size are same.

In this scenario we can create a single Modifier extension function with animation property and make it reusable, I will name it borderRevealAnimation( ).

fun Modifier.borderRevealAnimation() = composed {

    val animate = remember { Animatable(28f) }.apply {
        RevealEffectAnimation(this)
    }

    this.size(60.dp)
        .clip(CircleShape)
        .background(color = MaterialTheme.colors.surface)
        .border(
            width = Dp(animate.value),
            color = MaterialTheme.colors.onSurface,
            shape = CircleShape
        )
}

/**
 * @param animateShape shape which needs to be animated.
 * Animates between float 28f to 1f as of now for default args.
 */
@Composable
private fun RevealEffectAnimation(
    animateShape: Animatable<Float, AnimationVector1D>
) {
    LaunchedEffect(animateShape) {
        animateShape.animateTo(
            targetValue = 1f,
            animationSpec = repeatable(
                animation = tween(
                    durationMillis = 1000,
                    easing = LinearEasing,
                    delayMillis = 250
                ),
                repeatMode = RepeatMode.Restart,
                iterations = 1
            )
        )
    }
}

Now apply this modifier function to desired layout, in my case it is a Box.

Box(modifier = Modifier.borderRevealAnimation()) {
    Text()
}

Box(modifier = Modifier.borderRevealAnimation()) {
    Image()
}

Box(modifier = Modifier.borderRevealAnimation()) {
    Text()
}

That’s it.

Take a look at GitHub repository which I have linked below has complete code example on building advanced layouts, fetching data from api, animations, draw on canvas, search functionality, loader, UI state handling, Image loading, light/dark theme with MD3, palette usage etc.

Read all the detailed guidelines from Android documentation, it covers everything in much depth.

6. Project code and resources

Android Documentation on animations.

1. Animations with jetpack compose
2. Codelab on compose animations
3. Core compose animations android

Become Author

We are making content on new declarative UI kit Jetpack Compose, if you are willing to build content join us and we will help you get started.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.