DEV Community

Anatolii Frolov
Anatolii Frolov

Posted on • Originally published at proandroiddev.com

Pulse Indicator in Jetpack Compose — Ready-to-Use @Composable

Spinners are fine for generic loading. But for connectivity states like GPS, Bluetooth, or network, they often feel too abstract.

I wanted something more contextual than a spinner, so I built this small component.

Here’s a pulse indicator in Jetpack Compose: expanding rings around a central icon.

Pulse indicator preview

The Composable

@Composable
private fun PulseIndicator(
    @DrawableRes icon: Int,
    modifier: Modifier = Modifier
) {
    val periodMs = 3600L
    val offsetsMs = longArrayOf(0L, 1200L, 2400L)

    val startNs = remember { System.nanoTime() }
    var frameTimeNs by remember { mutableLongStateOf(startNs) }

    LaunchedEffect(Unit) {
        while (true) {
            withFrameNanos { now -> frameTimeNs = now }
        }
    }

    fun phase(offsetMs: Long): Float {
        val elapsedMs = (frameTimeNs - startNs) / 1_000_000L + offsetMs
        return ((elapsedMs % periodMs).toFloat() / periodMs.toFloat())
    }

    Box(modifier.size(80.dp), contentAlignment = Alignment.Center) {
        @Composable
        fun Ring(p: Float) = Box(
            Modifier
                .matchParentSize()
                .graphicsLayer {
                    scaleX = 1f + 0.8f * p
                    scaleY = 1f + 0.8f * p
                    alpha = 1f - p
                }
                .border(1.5.dp, Color.White.copy(alpha = 0.9f), CircleShape)
        )

        Ring(phase(offsetsMs[0]))
        Ring(phase(offsetsMs[1]))
        Ring(phase(offsetsMs[2]))

        Box(
            Modifier
                .size(80.dp)
                .background(Color.White, CircleShape),
            contentAlignment = Alignment.Center
        ) {
            Image(
                painter = painterResource(icon),
                contentDescription = null,
                modifier = Modifier.size(32.dp)
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

How it works

  • Animation timing: runs with withFrameNanos inside LaunchedEffect.
  • Phase calculation: three rings use different offsets (0, 1200, 2400 ms).
  • Rendering: rings expand and fade using graphicsLayer.
  • Core icon: central static anchor (e.g., a location pin).

Result: a smooth pulse effect that communicates connection status more clearly than a generic spinner.

📖 Originally published on ProAndroidDev (Medium):
Pulse Indicator in Jetpack Compose: Ready-to-Use @Composable

Top comments (0)