
This is a Android Jetpack Compose custom progress indicator UI design. It utilizes the power of Canvas to draw circles. It is an arc-shaped progress bar. You can customize the length, size, and gap between the two arcs below as per your requirements.
In Jetpack Compose there are two default progress indicators. They are linear and circular. They don’t have many customizations. Whenever we want to make a custom UI like this, we have to use Canvas API.
The Jetpack Compose custom progress indicator takes multiple parameters like startAngle, size, animation duration, the thickness of the arcs, etc… The UI contains text inside the Arc. It shows the data usage. The text style parameters are used to customize the text.
I have added animations to the progress bar using animateFloatAsState API. The foreground indicator moves forward and backward with a nice smooth animation. I have set the animation duration to 1 second. You can change it however you want.
Jetpack Compose offers a linear progress indicator. It is just a line that shows the value horizontally. You cannot modify its size, width, length, and other parameters. Another one is the circular progress bar. It is used for loading purposes. When you have a task running in the background, you can show this one to the user. You can also use it to show the percentage. But, sadly, it doesn’t have many customizations too.
Coming to the logic, the foreground indicator animates whenever the sweepAngle changes. The background indicator stays the same. I have set StrokeCap.Round style on both of them. By default, the startAngle is at 150f. Whenever you press the Animate with Random Value button, a random number will be generated and it is assigned to dataR variable. The dataR is a mutable one that remembers the state. As its value is changed, the whole UI will be recomposed again. Coming to the animation, targetValue equals dataR, and the animationSpec has a duration of 1000 milli seconds.
To use the code in your project, you need to download the latest Android Studio. Jetpack Compose doesn’t work in the older versions.
Related:
Final output:
If the video isn’t working, watch it on the YouTube.
Helpful links to understand the code:
Here are the Gradle files used in the project.
Code:
MainActivity.kt:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/*
You can use the following code for commercial purposes with some restrictions.
Read the full license here: https://semicolonspace.com/semicolonspace-license/
For more designs with source code,
visit: https://semicolonspace.com/jetpack-compose-samples/
*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BlogPostsTheme(darkTheme = false) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
ArcProgressbar()
}
}
}
}
}
@Composable
fun ArcProgressbar(
data: Float = 20f,
dataTextStyle: TextStyle = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_bold, FontWeight.Bold)),
fontSize = MaterialTheme.typography.h3.fontSize
),
remainingTextStyle: TextStyle = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_regular, FontWeight.Normal)),
fontSize = 16.sp
),
size: Dp = 250.dp,
thickness: Dp = 64.dp,
animationDuration: Int = 1000,
foregroundIndicatorColor: Color = Color(0xFF35898f),
backgroundIndicatorColor: Color = Color.LightGray.copy(alpha = 0.2f),
startAngle: Float = 150f,
dataPlanLimit: Float = 100f
) {
// It remembers the number value
var dataR by remember {
mutableStateOf(-1f)
}
val gapBetweenEnds = (startAngle - 90) * 2
// Number Animation
val animateNumber = animateFloatAsState(
targetValue = dataR,
animationSpec = tween(
durationMillis = animationDuration
)
)
// This is to start the animation when the activity is opened
LaunchedEffect(Unit) {
dataR = data
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(size = size)
) {
Canvas(
modifier = Modifier
.size(size = size)
) {
// Background Arc
drawArc(
color = backgroundIndicatorColor,
startAngle = startAngle,
sweepAngle = 360f - gapBetweenEnds,
useCenter = false,
style = Stroke(width = thickness.toPx(), cap = StrokeCap.Round)
)
// convert the number to angle
val sweepAngle = (animateNumber.value / dataPlanLimit) * (360f - gapBetweenEnds)
// Foreground circle
drawArc(
color = foregroundIndicatorColor,
startAngle = startAngle,
sweepAngle = sweepAngle,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Round)
)
}
// Display the data usage value
DisplayText(
animateNumber = animateNumber,
dataTextStyle = dataTextStyle,
remainingTextStyle = remainingTextStyle
)
}
Spacer(modifier = Modifier.height(32.dp))
ButtonProgressbar {
dataR = (0 until dataPlanLimit.toInt()).random().toFloat()
}
}
@Composable
private fun DisplayText(
animateNumber: State<Float>,
dataTextStyle: TextStyle,
remainingTextStyle: TextStyle
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Text that shows the number inside the circle
Text(
text = (animateNumber.value).toInt().toString() + " GB",
style = dataTextStyle
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "Remaining",
style = remainingTextStyle
)
}
}
@Composable
private fun ButtonProgressbar(
backgroundColor: Color = Color(0xFF35898f),
onClickButton: () -> Unit
) {
Button(
onClick = {
onClickButton()
},
colors = ButtonDefaults.buttonColors(
backgroundColor = backgroundColor
)
) {
Text(
text = "Animate with Random Value",
color = Color.White,
fontSize = 16.sp
)
}
}