What You Will Build
The iconic Matrix digital rain effect: columns of green characters falling at different speeds against a black background, with the leading character in white and trailing characters fading out. This uses Canvas with native Android Paint for high-performance text rendering.
Why This Pattern Matters
This effect is a rite of passage for graphics programmers. Building it in Compose teaches you Canvas drawing, frame-rate animation loops with LaunchedEffect, and efficient text rendering with native Paint. The same techniques apply to any real-time visualization: stock tickers, data visualizations, and game backgrounds.
Step 1: Define the Drop Model
data class MatrixDrop(
var y: Float,
val speed: Float,
val chars: List<Char>
)
Step 2: Initialize Columns
val config = LocalConfiguration.current
val columns = config.screenWidthDp / 16
val matrixChars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toList() +
"characters from other scripts for variety".toList()
val drops = remember {
(0 until columns).map {
MatrixDrop(
y = Random.nextFloat() * -500f,
speed = Random.nextFloat() * 3f + 2f,
chars = (0..20).map { matrixChars.random() }
)
}.toMutableStateList()
}
Step 3: Animation Loop
var tick by remember { mutableIntStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(50) // ~20 FPS
tick++
drops.forEachIndexed { index, drop ->
val newY = drop.y + drop.speed
drops[index] = drop.copy(
y = if (newY > config.screenHeightDp + 200)
Random.nextFloat() * -300f
else newY
)
}
}
}
Step 4: Canvas Rendering
Canvas(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
drops.forEachIndexed { col, drop ->
drop.chars.forEachIndexed { row, char ->
val x = col * 16f * density
val y = (drop.y + row * 18f) * density
if (y > 0 && y < size.height) {
val alpha = 1f -
(row.toFloat() / drop.chars.size)
drawContext.canvas.nativeCanvas.drawText(
char.toString(), x, y,
android.graphics.Paint().apply {
color = android.graphics.Color.argb(
(alpha * 255).toInt()
.coerceIn(25, 255),
if (row == 0) 255 else 0,
255,
if (row == 0) 255 else 0
)
textSize = 14f * density
typeface =
android.graphics.Typeface.MONOSPACE
}
)
}
}
}
}
Tips and Pitfalls
- Paint allocation: Creating Paint objects inside the draw loop is inefficient. In production, cache Paint objects and only change their alpha per character.
- Frame rate: 50ms delay gives ~20 FPS, which is perfect for this effect. Higher rates waste battery without visual improvement.
- White leading character: The
row == 0check makes the head of each column white, which is what gives the "falling" appearance. - Random reset: When a column falls below the screen, it resets to a random negative Y position. The negative range (-300f) creates natural staggering.
Min SDK: API 21+ with Compose 1.0+