
This is a Jetpack Compose progress bar UI with animation. It utilizes the Canvas API to draw the circles. There are two circles – foreground indicator and background indicator. The foreground indicator is the one that gets animated when the progress value is changed.
In Android, the Jetpack Compose has two default progress bars. They are linear and circular. Both of them doesn’t offer many customization options. You can’t change the indicator size, animation duration, width, etc… So, if you want to create your own custom progress bar, you have to use Canvas which offers a lot of customizations along with animation APIs.
Coming to the UI, there are two indicators. The foreground indicator has more stroke width and gets animated when you press the animate button. The background indicator has less stroke width and stays the same. I have applied StrokeCap.Round style to the background one. sweepAngle of the foreground one is the mutable variable. When its value is changed, the whole UI is recomposed.
The Jetpack Compose progress bar which has animation has both Canvas and Text. They are inside the Box with center alignment. The text shows the progress value. It has also animateFloatAsState animation. You can change the animation duration and tween according to your requirements.
If you want to use this code, you need to download the latest Android Studio. This is because the 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:
- Canvas drawArc
- animateFloatAsState
- What is Modifier in Jetpack Compose?
- Kotlin Higher Order Functions
Here are the Gradle files used in the project.
Here is the Source 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.Canvas
import androidx.compose.foundation.background
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.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: http://semicolonspace.com/semicolonspace-license/
For more designs with source code,
visit: http://semicolonspace.com/jetpack-compose-samples/
*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BlogPostsTheme(darkTheme = false) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.White),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressbar2()
}
}
}
}
}
@Composable
fun CircularProgressbar2(
number: Float = 60f,
numberStyle: TextStyle = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 28.sp
),
size: Dp = 180.dp,
thickness: Dp = 16.dp,
animationDuration: Int = 1000,
animationDelay: Int = 0,
foregroundIndicatorColor: Color = Color(0xFF35898f),
backgroundIndicatorColor: Color = foregroundIndicatorColor.copy(alpha = 0.5f),
extraSizeForegroundIndicator: Dp = 12.dp
) {
// It remembers the number value
var numberR by remember {
mutableStateOf(-1f)
}
// Number Animation
val animateNumber = animateFloatAsState(
targetValue = numberR,
animationSpec = tween(
durationMillis = animationDuration,
delayMillis = animationDelay
)
)
// This is to start the animation when the activity is opened
LaunchedEffect(Unit) {
numberR = number
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(size = size)
) {
Canvas(
modifier = Modifier
.size(size = size)
) {
// Background circle
drawCircle(
color = backgroundIndicatorColor,
radius = size.toPx() / 2,
style = Stroke(width = thickness.toPx(), cap = StrokeCap.Round)
)
val sweepAngle = (animateNumber.value / 100) * 360
// Foreground circle
drawArc(
color = foregroundIndicatorColor,
startAngle = -90f,
sweepAngle = sweepAngle,
useCenter = false,
style = Stroke((thickness + extraSizeForegroundIndicator).toPx(), cap = StrokeCap.Butt)
)
}
// Text that shows number inside the circle
Text(
text = (animateNumber.value).toInt().toString(),
style = numberStyle
)
}
Spacer(modifier = Modifier.height(32.dp))
ButtonProgressbar {
numberR = (1..100).random().toFloat()
}
}
@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
)
}
}