Skip to content

Instantly share code, notes, and snippets.

@iprashantpanwar
Last active October 17, 2025 17:42
Show Gist options
  • Select an option

  • Save iprashantpanwar/8fdff811cb3c225a79dab0e140d38ed7 to your computer and use it in GitHub Desktop.

Select an option

Save iprashantpanwar/8fdff811cb3c225a79dab0e140d38ed7 to your computer and use it in GitHub Desktop.
Bouncy, pulsating heart animation in Jetpack Compose — inspired by https://dribbble.com/shots/2419184-Heart-Rate-Monitor-Animation
@Composable
fun HeartBeatScreen(
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
AnimatedLayeredHearts(modifier)
}
}
@Composable
fun AnimatedLayeredHearts(
modifier: Modifier = Modifier,
heartColor: Color = Color(0xFFFF5A5F),
centerSize: Dp = 64.dp
) {
val baseScales = listOf(4.5f, 2.5f, 1f)
val alphas = listOf(0.1f, 0.2f, 1f)
val beatDuration = 500
val delayBetween = 50
val animatedScales = remember {
baseScales.map { Animatable(1f) }
}
// Launch staggered animations
LaunchedEffect(Unit) {
while (true) {
// Animate all hearts with staggered launch
animatedScales.indices.reversed().forEach { actualIndex ->
val anim = animatedScales[actualIndex]
launch {
delay((animatedScales.size - 1 - actualIndex) * delayBetween.toLong())
anim.animateTo(
1.25f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
anim.animateTo(
1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessLow
)
)
}
}
// Pause after all animations complete
val totalCycleTime =
delayBetween * (animatedScales.size - 1) + beatDuration // approx total duration
delay(totalCycleTime + 700L) // 1-second rest after one full pulse cycle
}
}
Box(modifier = modifier, contentAlignment = Alignment.Center) {
animatedScales.zip(baseScales).zip(alphas).forEach { (scalePair, alpha) ->
val (animScale, baseScale) = scalePair
HeartShape(
modifier = Modifier
.size(centerSize * baseScale)
.scale(animScale.value),
color = heartColor.copy(alpha = alpha)
)
}
}
}
@Composable
fun HeartShape(
modifier: Modifier = Modifier,
color: Color
) {
Canvas(modifier = modifier) {
val width = size.width
val height = size.height
val path = Path().apply {
moveTo(width / 2f, height * 1.1f)
cubicTo(
width * 1.4f, height * 0.6f,
width * 1.1f, height * -0.15f,
width / 2f, height * 0.2f
)
cubicTo(
-width * 0.1f, height * -0.15f,
-width * 0.4f, height * 0.6f,
width / 2f, height * 1.1f
)
}
drawPath(path = path, color = color, style = Fill)
}
}
@Preview(showBackground = true)
@Composable
fun PreviewStaticLayeredHearts() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
contentAlignment = Alignment.Center
) {
AnimatedLayeredHearts()
}
}
@iprashantpanwar
Copy link
Author

bouncy_heart.webm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment