Blog Infos
Author
Published
Topics
Published

In this article, I’ll show you how to build the circular reveal animation WhatsApp uses with Jetpack Compose.

1. Using AnimatedVisibility

My first attempt was to use AnimatedVisibility to achieve something similar.

AnimatedVisibility(
    visible = visible,
) {
    BottomSheet()
}

This code is equivalent to:

AnimatedVisibility(
    visible = visible,
    enter = expandVertically(),
    exit = shrinkVertically()
) {
    BottomSheet()
}

However, I was not able to achieve the circular motion I was looking for.

2. Using GenericShape

My second attempt was to use a shape to clip my composable to. GenericShape allows me to write whatever shape I want but there’s nothing that allows me to draw a circle so I started with a rectangle.

GenericShape { size, direction ->
    val center = size.width / 2f
    this.addRect(
        Rect(
            left = center - center * it,
            top = center - center * it,
            right = center + center * it,
            bottom = size.height
        )
    )
    close()
}

I got something to animate but it still missed the circular motion I was looking for.

3. Using addArc

Given that a rectangle is not what I want I tried using addArc to draw an arc around the rectangle.

GenericShape { size, direction ->
    val centerV = size.height / 2f
    val centerH = size.width / 2f
    addArc(
        Rect(
            left = centerH - centerH * it * 2,
            top = size.height - size.height * it,
            right = centerH + centerH * it * 2,
            bottom = size.height + size.height * it
        ),
        0f,
        -180f
    )
    close()
}

The result was much better, however, I couldn’t get it to expand fully so there was always a part of the component that was cropped out.

4. Using cubicTo

What if instead of using a normal arc I used a more “complicated” one? That’s when I tried to use cubicTo, it uses a cubic bézier curve. It has 4 control points allowing me to draw something that looks like semicircle.

If I implemented a cubic bézier curve that has the same size as my composable then the top corners would still be cropped out so I needed a way to make the curve wrap my whole composable.

To do that I made the curve twice as big as the composable. For the x axis that’s done by multiplying the width by 2, for the y axis I’m using a lerp function that goes to from height to -height.

GenericShape { size, direction ->
    val centerH = size.width / 2f
 moveTo(
        x = centerH - centerH * it * 2,
        y = size.height,
    )
    val currentHeight = lerp(size.height, -size.height, it)
    cubicTo(
        x1 = centerH - centerH * it,
        y1 = currentHeight,
        x2 = centerH + centerH * it,
        y2 = currentHeight,
        x3 = centerH + centerH * it * 2,
        y3 = size.height,
    )
    close()
}

The result was pretty close to what I wanted but the shape is not growing proportionally.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Jetpack Compose: Drawing without pain and recomposition

This is a talk on recomposition in Jetpack Compose and the myths of too many calls it is followed by. I’ll briefly explain the reasons behind recompositions and why they are not as problematic as…
Watch Video

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jobs

I kept tweaking the code until I arrived at something that was pretty close to the animation WhatsApp is using.

GenericShape { size, _ ->
    val centerH = size.width / 2f
    val multiplierW = 1.5f + size.height / size.width
    moveTo(
        x = centerH - centerH * progress * multiplierW,
        y = size.height,
    )
    val currentWidth = (centerH * progress * multiplierW * 2.5f)
    cubicTo(
        x1 = centerH - centerH * progress * 1.5f,
        y1 = size.height - currentWidth * 0.5f,
        x2 = centerH + centerH * progress * 1.5f,
        y2 = size.height - currentWidth * 0.5f,
        x3 = centerH + centerH * progress * multiplierW,
        y3 = size.height,
    )
    close()
}

There are some magic numbers in the calculations and I’m not sure why they work, however, the end result looks pretty similar to what WhatsApp is using.

Bonus

If you take a look at the WhatsApp animation again you might notice that the items inside the card also animate a bit. Their scale at the start is probably 90%, then it goes up to 110% and finally goes down to 100%.

To implement that I used animateFloatAsState.

var scale by remember { mutableStateOf(0.9f) }
val animation = animateFloatAsState(
    targetValue = scale,
    animationSpec = FloatSpringSpec(
        dampingRatio = 0.3f,
    )
)
LaunchedEffect(Unit) {
    delay(20 + position.toLong() * 20)
    scale = 1f
}
Image(
    modifier = Modifier
       ...
        .graphicsLayer {
            scaleX = animation.value
            scaleY = animation.value
        }
)

It’s a simple float animation that goes from 0.9 to 1 and uses a spring animation spec. I’m adding a 20ms delay + another delay based on the item position so everything doesn’t animate at the same time. If you want something to appear later then just give it a higher position.

It doesn’t look exactly like the one WhatsApp uses but if you can continue tweaking the delay and the dampingRatio value until you get something you’re happy with.

If you want to check the source code, you can find it here.

If you have any comments or suggestions, please reach out to me on Twitter.

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu