What You Will Build
A mathematically-drawn heart shape on Canvas that pulses continuously with an infinite transition and explodes in scale when tapped, using spring physics. This combines parametric math curves, Canvas drawing, infinite animations, and gesture handling in a single composable.
Why This Pattern Matters
Like buttons on Instagram, Twitter, and TikTok all use heart animations. The parametric heart curve teaches you mathematical drawing on Canvas, while the spring-based tap animation is the standard pattern for micro-interactions in social media apps.
Step 1: Set Up the Animations
@Composable
fun HeartAnimationScreen() {
// Tap-triggered spring animation
val scale = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
// Continuous pulse
val infiniteTransition = rememberInfiniteTransition(label = "heart")
val pulse by infiniteTransition.animateFloat(
1f, 1.15f,
infiniteRepeatable(tween(600), RepeatMode.Reverse),
label = "pulse"
)
Step 2: Draw the Parametric Heart
The heart shape uses the parametric equations: x = 16sin^3(t), y = 13cos(t) - 5cos(2t) - 2cos(3t) - cos(4t):
Canvas(Modifier.size(200.dp)) {
val cx = size.width / 2
val cy = size.height / 2
val heartSize = 80f * pulse * scale.value
val path = Path()
for (t in 0..360) {
val rad = Math.toRadians(t.toDouble())
val x = 16f * sin(rad).toFloat().pow(3) * heartSize / 16f
val y = -(13f * cos(rad).toFloat()
- 5f * cos(2 * rad).toFloat()
- 2f * cos(3 * rad).toFloat()
- cos(4 * rad).toFloat()) * heartSize / 16f
if (t == 0) path.moveTo(cx + x, cy + y)
else path.lineTo(cx + x, cy + y)
}
path.close()
drawPath(path, Color(0xFFFF1744))
}
Step 3: Add Tap-to-Bounce Gesture
Box(
Modifier.fillMaxSize()
.background(Color(0xFF1A1A2E))
.pointerInput(Unit) {
detectTapGestures {
scope.launch {
scale.animateTo(1.5f,
spring(dampingRatio = 0.3f))
scale.animateTo(1f, spring())
}
}
},
contentAlignment = Alignment.Center
) {
// Canvas goes here
}
Tips and Pitfalls
- Animatable vs animateFloatAsState: Animatable supports sequential animations (scale up then down) via coroutines. animateFloatAsState only animates to a single target.
- Spring dampingRatio = 0.3f creates the bouncy overshoot. Default (1.0) would ease in without bounce.
- The negative Y in the equation flips the heart right-side up (Canvas Y increases downward).
- Performance: Drawing 360 line segments per frame is fine for a single shape. For multiple hearts, pre-compute the path and cache it with remember.
Min SDK: 21 | Compose BOM: 2024.01.00+