
Introduction
A pulse indicator is a simple but powerful UI element that helps visualize connectivity or activity status. Unlike a loading spinner, it conveys the idea of a signal radiating from a center point, which is especially useful for GPS or network connection states.
In this article we will look at a ready-to-use implementation in Jetpack Compose. The code is minimal, efficient, and easy to integrate into your app. By the end, you will see exactly how it works and how it looks in a real application.
Result preview
Before diving into the code, here is how the pulse indicator looks in action:

The Code
Here is the full composable function that renders the pulse indicator:
@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)
)
}
}
}
How It Works
Animation timing
The composable uses withFrameNanos inside a LaunchedEffect. This gives access to the current frame timestamp and ensures the animation runs smoothly while the composable is on screen. When it leaves composition, the coroutine is automatically cancelled.
Phase calculation
The function phase(offsetMs) converts the elapsed time into a value between 0f and 1f. Each ring has a different offset (0, 1200, 2400 ms), so they expand at different moments. This creates the illusion of continuous waves.
Ring rendering
Each ring is drawn as a Box with a circular border. Its size and opacity are modified using graphicsLayer:
scaleXandscaleYgradually grow from1fto1.8f.alphafades out from1fto0f.
Together, this creates an expanding, fading circle.
Core icon
In the center, a solid white circle holds the provided icon (for example, a location pin). This acts as the static anchor point while the animated rings pulse outward.
How It Looks in a Real App
When you use this indicator in a screen that represents GPS connection, you get a clear and intuitive visual. For example:

- While trying to connect, the background can be gray, and the pulse shows activity.
- Once connected, the background turns into an active gradient, and the pulse continues around the location pin.
This gives users immediate feedback without needing to read extra text.
Job Offers
Conclusion
The PulseIndicator is a lightweight and reusable Jetpack Compose component. It uses a single animation source (withFrameNanos), three rings with time offsets, and a central icon. The result is a smooth pulse effect that communicates connection status much better than a generic loading spinner.
You can drop this composable into any Compose screen and instantly provide a modern, engaging visual indicator for GPS, Bluetooth, or any other type of connectivity.
If you found this article useful, consider following me here on Medium for more practical Android development content.
You might also like:

Anatolii Frolov
Senior Android Developer
Writing honest, real-world Kotlin & Jetpack Compose insights.
📬 Follow me on Medium
This article was previously published on proandroiddev.com



