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
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