What You Will Build
A full-screen starfield with 200 twinkling stars drifting horizontally and a shooting star trailing across the sky. The entire scene is rendered on a single Canvas with no individual composables for each star, making it highly performant.
Why This Pattern Matters
Particle systems and ambient background animations add polish to apps. Drawing 200+ moving objects as composables would tank performance, but a single Canvas handles them efficiently. This same pattern applies to snow, rain, confetti, and any ambient particle effect.
Step 1: Star Data Class
data class Star(
val x: Float, // normalized 0-1
val y: Float, // normalized 0-1
val size: Float, // radius in pixels
val speed: Float // drift speed multiplier
)
val stars = remember {
List(200) {
Star(
Random.nextFloat(),
Random.nextFloat(),
Random.nextFloat() * 3f + 0.5f,
Random.nextFloat() * 2f + 0.5f
)
}
}
Step 2: Animate Time
val infiniteTransition = rememberInfiniteTransition(label = "space")
val time by infiniteTransition.animateFloat(
0f, 1f,
infiniteRepeatable(tween(5000, easing = LinearEasing)),
label = "t"
)
Step 3: Draw Stars and Shooting Star
Canvas(Modifier.fillMaxSize().background(Color(0xFF000011))) {
// Twinkling stars
stars.forEach { star ->
val x = ((star.x + time * star.speed * 0.1f) % 1f) * size.width
val y = star.y * size.height
val twinkle = (0.5f + 0.5f * kotlin.math.sin(
(time * 10f + star.x * 100f).toDouble()
)).toFloat()
drawCircle(
Color.White.copy(alpha = twinkle),
star.size,
Offset(x, y)
)
}
// Shooting star with trail
val shootX = (time * size.width * 2f) % (size.width * 1.5f)
val shootY = size.height * 0.2f + shootX * 0.3f
if (shootX < size.width) {
for (t in 0..20) {
drawCircle(
Color.White.copy(alpha = (1f - t / 20f) * 0.8f),
2f - t * 0.08f,
Offset(shootX - t * 8f, shootY - t * 2.4f)
)
}
}
}
Tips and Pitfalls
- Normalized coordinates (0-1) for star positions let you multiply by
size.width/heightat draw time, making the animation resolution-independent. - sin() for twinkling creates a smooth oscillation. The star.x offset staggers the phase so all stars do not twinkle in sync.
- Modulo wrapping ((x + offset) % 1f) makes stars loop seamlessly from right to left.
- Shooting star trail: 20 circles with decreasing alpha and size create a gradient tail effect.
Min SDK: 21 | Compose BOM: 2024.01.00+