Creating a Smooth Animated Progress Bar in Jetpack Compose: Canvas drawing and Gradient Animation

Kappdev
5 min readMay 2, 2024

Welcome 👋

Have you ever wanted to take your app’s user experience to the next level? This article will explore how to create a customizable and captivating Animated Progress Bar.

Forget boring progress bars! Skyrocket the visual appeal of your app 🚀

The Composable Function

Let’s start by defining the AnimatedProgressBar composable function:

@Composable
fun AnimatedProgressBar(
progress: Float,
modifier: Modifier,
colors: List<Color>,
trackBrush: Brush? = SolidColor(Color.Black.copy(0.16f)),
strokeWidth: Dp = 4.dp,
glowRadius: Dp? = 4.dp,
strokeCap: StrokeCap = StrokeCap.Round,
gradientAnimationSpeed: Int = 2500,
progressAnimSpec: AnimationSpec<Float> = tween(
durationMillis = 720,
easing = LinearOutSlowInEasing
)
) {
// Implementation…
}

⚒️ ️️Parameters breakdown

progress ➜ The current progress value (0f to 1f).

modifier ➜ The modifier to be applied to the progress bar.

colors ➜ List of colors defining the gradient animation.

trackBrush ➜ Brush for the static track behind the progress line. If null, no track will be drawn.

strokeWidth ➜ Controls the thickness of the progress line.

glowRadius ➜ Adds a glow effect to the progress line. If null, no effect will be applied.

strokeCap ➜ Defines the style of the line ends.

gradientAnimationSpeed ➜ Duration (in milliseconds) of one loop of the gradient animation.

progressAnimSpec ➜ Animation behavior for the progress line on value changes.

Implementation ✨

Alright, now we can proceed to the implementation.

Animated Gradient

Let’s begin by crafting the main animation: Moving gradient.

So, how do we do that🤔

We create a linear horizontal gradient with colors positioned at specific locations (0 to 1) within the gradient. Then, we animate an offset value from 0 to 1 and restart continuously.

This animation adjusts the positions of each color within the gradient definition. If a color’s position goes beyond 1, we subtract 1 to wrap it around and place it back at the beginning (0).

As a result, the gradient appears to move smoothly to the end, jump back to the start, and repeat the loop, creating a visually appealing animation.

Time to view the code 👀

// Creat an infinite animation transition
val infiniteTransition = rememberInfiniteTransition()

// Animates offset value transition from 0 to 1
val offset by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = gradientAnimationSpeed,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)

// Creates a brush that updates based on the animated offset
val brush: ShaderBrush by remember(offset) {
object : ShaderBrush() {
override fun createShader(size: Size): Shader {
val step = 1f / colors.size // Calculate step size
val start = step / 2 // Define start position

// Calculate original positions for each color
val originalSpots = List(colors.size) { start + (step * it) }

// Apply animation offset to each color position
val transformedSpots = originalSpots.map { spot ->
val shiftedSpot = (spot + offset)
// Wrap around if exceeds 1
if (shiftedSpot > 1f) shiftedSpot - 1f else shiftedSpot
}

// Combine colors with their transformed positions
val pairs = colors.zip(transformedSpots).sortedBy { it.second }

// Margin for gradient outside the progress bar
val margin = size.width / 2

// Create the linear gradient shader with colors and positions
return LinearGradientShader(
colors = pairs.map { it.first },
colorStops = pairs.map { it.second },
from = Offset(-margin, 0f),
to = Offset(size.width + margin, 0f)
)
}
}
}

💡To create a smooth, uninterrupted animation of the gradient, we draw it wider than the actual progress bar. This is achieved by adding a margin to both ends of the gradient.

Progress Animation

Before we step into drawing the progress bar, let’s write a little animation that will smoothly transition between progress values.

val animatedProgress by animateFloatAsState(
targetValue = progress.coerceIn(0f, 1f),
animationSpec = progressAnimSpec
)

Canvas Drawing

Finally, we can utilize the Canvas composable to draw the progress bar.

Canvas(modifier) {
val width = this.size.width
val height = this.size.height

// Create a Paint object
val paint = Paint().apply {
// Enable anti-aliasing for smoother lines
isAntiAlias = true
style = PaintingStyle.Stroke
strokeWidth = strokeWidth.toPx()
strokeCap = strokeCap
// Apply the animated gradient shader
shader = brush.createShader(size)
}

// Handle optional glow effect
glowRadius?.let { radius ->
paint.asFrameworkPaint().apply {
setShadowLayer(radius.toPx(), 0f, 0f, android.graphics.Color.WHITE)
}
}

// Draw the track line if specified
trackBrush?.let { tBrush ->
drawLine(
brush = tBrush,
start = Offset(0f, height / 2f),
end = Offset(width, height / 2f),
cap = strokeCap,
strokeWidth = strokeWidth.toPx()
)
}

// Draw the progress line if progress is greater than 0
if (animatedProgress > 0f) {
drawIntoCanvas { canvas ->
canvas.drawLine(
p1 = Offset(0f, height / 2f),
p2 = Offset(width * animatedProgress, height / 2f),
paint = paint
)
}
}
}

Congratulations🥳! We’ve successfully built it👏. For the complete code implementation, you can access it on GitHub Gist🧑‍💻. Now, let’s explore how we can put it to use.

Advertisement

Are you learning a foreign language and struggling with new vocabulary? Then, I strongly recommend you check out this words-learning app, which will make your journey easy and convenient!

WordBook

Usage

Here’s a practical example demonstrating how to use the AnimatedProgressBar with button clicks to simulate progress updates.

// Define the gradient colors
val GradientColors = listOf(
Color.Red,
Color.Yellow,
Color.Green,
Color.Cyan,
Color.Blue,
Color.Magenta
)
var progress by remember { mutableFloatStateOf(0f) }

Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
AnimatedProgressBar(
progress = progress,
modifier = Modifier.fillMaxWidth(0.9f),
colors = GradientColors
)

Button(
onClick = {
if (progress < 1f) progress += 0.1f else progress = 0f
}
) {
Text("Update Progress")
}
}

The Result 😍

You might also like 👇

Thank you for reading this article!❤️ I hope you’ve found it enjoyable and valuable. Feel free to show your appreciation by hitting the clap👏 if you liked it or follow Kappdev for more exciting articles😊

Happy coding!

--

--

Kappdev

💡 Curious Explorer 🧭 Kotlin and Compose enthusiast 👨‍💻 Passionate about self-development and growth ❤️‍🔥 Push your boundaries 🚀