Jetpack Compose is Android’s modern UI toolkit, offering a declarative and efficient way to build user interfaces. One common UI element is the progress bar, used to indicate the status of a loading process or any operation in progress. While Jetpack Compose provides standard progress bars, you can also create custom progress bars to match your app’s unique design and branding.
Why Create Custom Progress Bars in Jetpack Compose?
- Branding: Align the progress bar’s look and feel with your app’s design.
- Unique Appearance: Create a distinct visual experience that stands out.
- Flexibility: Add custom animations and interactive elements.
Implementing Custom Progress Bars
You can implement custom progress bars in Jetpack Compose using various techniques, including:
- Using shapes and Canvas
- Animating progress using
animateFloatAsState
- Composing different UI elements to create the desired effect
Example 1: Simple Linear Progress Bar with Custom Colors
Here’s how to create a simple linear progress bar with custom colors:
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun CustomLinearProgressBar(
progress: Float, // Progress value between 0 and 1
color: Color = MaterialTheme.colorScheme.primary,
backgroundColor: Color = Color.LightGray
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(10.dp)
) {
// Background line
drawRoundRect(
color = backgroundColor,
topLeft = Offset.Zero,
size = Size(width = size.width, height = size.height),
cornerRadius = CornerRadius(5.dp.toPx(), 5.dp.toPx())
)
// Progress line
drawRoundRect(
color = color,
topLeft = Offset.Zero,
size = Size(width = size.width * progress, height = size.height),
cornerRadius = CornerRadius(5.dp.toPx(), 5.dp.toPx())
)
}
}
@Preview(showBackground = true)
@Composable
fun CustomLinearProgressBarPreview() {
var progress by remember { mutableStateOf(0.5f) } // Example progress value
// Simulate progress animation
LaunchedEffect(key1 = Unit) {
progress = 0.8f
}
CustomLinearProgressBar(progress = progress)
}
In this example:
- A
Canvas
is used to draw the progress bar. - The
progress
parameter controls how much of the bar is filled. drawRoundRect
is used to create rounded corners.- The colors are customizable.
Example 2: Animated Circular Progress Bar
Here’s how to create a circular progress bar with a rotating animation:
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun AnimatedCircularProgressBar(
progress: Float, // Progress value between 0 and 1
strokeWidth: Dp = 8.dp,
color: Color = MaterialTheme.colorScheme.primary,
backgroundColor: Color = Color.LightGray
) {
val animatedProgress = animateFloatAsState(
targetValue = progress,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing)
).value
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(100.dp)
) {
Canvas(
modifier = Modifier.size(100.dp)
) {
// Background circle
drawCircle(
color = backgroundColor,
radius = size.minDimension / 2,
center = Offset(x = size.width / 2, y = size.height / 2)
)
// Animated progress arc
drawArc(
color = color,
startAngle = -90f, // Start from the top
sweepAngle = 360 * animatedProgress,
useCenter = false,
style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
)
}
}
}
@Preview(showBackground = true)
@Composable
fun AnimatedCircularProgressBarPreview() {
var progress by remember { mutableStateOf(0.7f) }
// Simulate progress animation
LaunchedEffect(key1 = Unit) {
progress = 0.9f
}
AnimatedCircularProgressBar(progress = progress)
}
In this example:
animateFloatAsState
is used to animate the progress value smoothly.drawArc
is used to draw the circular progress arc.- Customizable
strokeWidth
and colors are provided. - The
stroke
style is used to create a hollow circle.
Example 3: Determinate Progress Bar
This example demonstrates a determinate progress bar that visually fills as the progress value changes:
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun DeterminateCircularProgressBar(
progress: Float, // Progress value between 0 and 1
strokeWidth: Dp = 8.dp,
color: Color = MaterialTheme.colorScheme.primary,
backgroundColor: Color = Color.LightGray,
animationDuration: Int = 1000
) {
val animatedProgress = animateFloatAsState(
targetValue = progress,
animationSpec = tween(durationMillis = animationDuration, easing = LinearEasing),
label = "Determinate Progress Animation"
).value
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(100.dp)
) {
Canvas(
modifier = Modifier.size(100.dp)
) {
// Background circle
drawCircle(
color = backgroundColor,
radius = size.minDimension / 2,
center = Offset(x = size.width / 2, y = size.height / 2)
)
// Animated progress arc
drawArc(
color = color,
startAngle = -90f, // Start from the top
sweepAngle = 360 * animatedProgress,
useCenter = false,
style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
)
}
Text(
text = "${(animatedProgress * 100).toInt()}%",
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
}
@Preview(showBackground = true)
@Composable
fun DeterminateCircularProgressBarPreview() {
var progress by remember { mutableStateOf(0.0f) }
// Simulate progress animation
LaunchedEffect(key1 = Unit) {
progress = 0.75f //Example: Set progress to 75%
}
DeterminateCircularProgressBar(progress = progress)
}
This code performs the following:
- Animates a circular progress bar, transitioning to a given
progress
value over time. - The current progress is displayed as a percentage in the center of the circle, providing real-time feedback to the user.
Example 4: Indeterminate Progress Bar
Here is how to implement an indeterminate progress bar which runs a repeating animation for use when progress is unknown:
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun IndeterminateCircularProgressBar(
strokeWidth: Dp = 8.dp,
color: Color = MaterialTheme.colorScheme.primary,
backgroundColor: Color = Color.LightGray,
animationDuration: Int = 1000
) {
val animatedFloat = remember { Animatable(0f) }
// Animate the progress bar indefinitely.
LaunchedEffect(animatedFloat) {
animatedFloat.animate(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = animationDuration, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
}
val animatedProgress = animatedFloat.value
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(100.dp)
) {
Canvas(
modifier = Modifier.size(100.dp)
) {
// Background circle
drawCircle(
color = backgroundColor,
radius = size.minDimension / 2,
center = Offset(x = size.width / 2, y = size.height / 2)
)
val startAngle = animatedProgress * 360f
// Animated progress arc
drawArc(
color = color,
startAngle = startAngle,
sweepAngle = 90f,
useCenter = false,
style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
)
}
}
}
@Preview(showBackground = true)
@Composable
fun IndeterminateCircularProgressBarPreview() {
IndeterminateCircularProgressBar()
}
This indeterminate progress bar continuously animates the progress by changing the startAngle
, creating a dynamic and visually appealing loading indicator.
Conclusion
Creating custom progress bars in Jetpack Compose enables you to enhance your app’s UI by matching progress indicators to your specific branding and design needs. Using techniques such as Canvas drawing and animations, you can produce a variety of progress bars to enrich the user experience. Each of the given examples demonstrates essential aspects of making both determinate and indeterminate progress bars, granting both information and engagement within the user interface. These can also be adjusted to satisfy a range of application scenarios. With these custom progress bars, Jetpack Compose gives you greater control to tailor the feel and look of the app’s progress and status indicators, consequently enhancing the experience for the user.