TLDR

I created a customizable wave progress bar using Jetpack Compose, as shown below. You can view the code on Github. The component can be wrapped within any Jetpack View (here, it’s wrapped in a circular card). You can easily adjust various properties of the progress bar, including frequency, amplitude, direction, and speed, to fit your design needs.

Wave Progress Bar

Implementation

Sine Wave

The closest way to render a wave-like animation is by using a sine wave, which can be drawn with the Android path and sin APIs. The utility function below generates a sine wave path, allowing for customization through various parameters.

fun prepareSinePath(
    path: Path,
    size: Size,
    frequency: Int,
    amplitude: Float,
    phaseShift: Float,
    position: Float,
    step: Int
) {
    for (x in 0..size.width.toInt().plus(step) step step) {
        val y = position + amplitude * sin(x * frequency * Math.PI / size.width + phaseShift).toFloat()
        if (path.isEmpty)
            path.moveTo(x.toFloat(), max(0f, min(y, size.height)))
        else
            path.lineTo(x.toFloat(), max(0f, min(y, size.height)))
    }
}

You can call the utility function within the drawBehind API of a modifier to draw the sine wave (parameter definitions are omitted for brevity).

Box(
    modifier = modifier
        .drawBehind {
            val yPos = (1 - progress) * size.height
            Path()
                .apply {
                    prepareSinePath(this, size, waveFrequency, amplitude, phaseShift, yPos, waveSteps)
                }
                .also {
                    drawPath(path = it, brush = SolidColor(Color.Cyan), style = Stroke(width = 10f))
                }
        }
)
Sine Wave

Complete Path

To fill the entire area from the left bottom to the right bottom of the sine wave, start the path at the left bottom, end it at the right bottom, and then close it. This can be achieved using the lineTo and close functions of the Path object.

{
    prepareSinePath(this, size, waveFrequency, amplitude.value, phaseShiftLocal, yPos, waveSteps)
    lineTo(size.width, size.height)
    lineTo(0f, size.height)
    close()
}
Complete Path

We can use Gradient style to fill it out.

drawPath(path = it, brush = Brush.horizontalGradient(listOf(Color.Magenta, Color.Cyan), style = Fill)
Gradient Path

Animation

To make the sine wave feel dynamic, we need to add multiple animations. One animation continuously changes the wave’s amplitude from low to high, while another moves the wave horizontally. This can be achieved using the animateTo API along with LaunchedEffect (to allow dynamic parameter updates from outside the component).

LaunchedEffect(amplitudeRange, amplitudeDuration) {
    coroutineScope.launch {
        amplitude.stop()
        amplitude.snapTo(amplitudeRange.start)
        amplitude.animateTo(
            targetValue = amplitudeRange.endInclusive,
            animationSpec = infiniteRepeatable(
                animation = tween(durationMillis = amplitudeDuration, easing = LinearEasing),
                repeatMode = RepeatMode.Reverse
            )
        )
    }
}

LaunchedEffect(phaseShiftDuration) {
    coroutineScope.launch {
        phaseShift.stop()
        phaseShift.snapTo(0f)
        phaseShift.animateTo(
            targetValue = (2 * PI).toFloat(),
            animationSpec = infiniteRepeatable(
                animation = tween(durationMillis = phaseShiftDuration, easing = LinearEasing),
                repeatMode = RepeatMode.Restart
            )
        )
    }
}
Wave Progress Bar

Library

The wave progress component is implemented as a separate module, available here. The repository includes an Activity with sliders to adjust various configurable parameters of the component.

Customizations

Thank you!

I have uploaded complete code here in case you want to run it locally or use it in one of your projects. Remember to ⭐ if you like it!