Jetpack Compose Circular Progress bar with Source Code

jetpack compose circular progress bar

This is a Jetpack Compose circular progress bar UI design. It is made with Canvas to draw the circles. The circles get animated using the animateFloatAsState API. You can download the source code for free (below).

In Android, the default Jetpack Compose has two progress bars. They are linear and circular. They do not have many customization options. You cannot change the indicator size, color, and animation durations. So, if you want to make your own custom progress bar, you have to use Canvas API.

Coming to the UI, I have drawn 3 circles using the drawCircle and drawArc methods. The first one is the foreground indicator which gets animated every time the progress value is changed. The second one is the background indicator which stays the same. It has white color. The third one is for the shadow. I set the radial gradient using the Brush.radialGradient method. It contains the text in the middle of the circles. For that, I put the Canvas and Text inside the Box with contentAlignment equals Alignment.Center.

The Jetpack Compose circular progress bar takes size, indicator color, shadow color, thickness, animation duration, and text styles. You can adjust them according to your requirements. I set the Roboto font. You can download it from the Google fonts website.

There are very few customization options with the default linear and circular progress bars. But, if you use Canvas, you can create any UI in any way you want. It also offers animation APIs to make the UI look beautiful. I have added “Animate with Random Value” which generates a random value when you tap on it. Based on the value, the UI will be recomposed and the foreground indicator updates the value with a nice animation.

Make sure that you are using the latest Android Studio version. Because the Jetpack Compose doesn’t work in the older versions.

    Final Output:

    If the video isn’t working, watch it on the YouTube.

    Helpful Links to Understand the Code:

    Resources Used in the Project:

    Here are the Gradle files used in the project.

    Download the Roboto font from Google Fonts site.

    Jetpack Compose Circular Progress Bar 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.geometry.Offset
    import androidx.compose.ui.geometry.Size
    import androidx.compose.ui.graphics.Brush
    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()
                            .background(color = Color.White),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        CircularProgressbar1()
                    }
                }
            }
        }
    }
    
    @Composable
    fun CircularProgressbar1(
        size: Dp = 260.dp,
        foregroundIndicatorColor: Color = Color(0xFF35898f),
        shadowColor: Color = Color.LightGray,
        indicatorThickness: Dp = 24.dp,
        dataUsage: Float = 60f,
        animationDuration: Int = 1000,
        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
        )
    ) {
        // It remembers the data usage value
        var dataUsageRemember by remember {
            mutableStateOf(-1f)
        }
    
        // This is to animate the foreground indicator
        val dataUsageAnimate = animateFloatAsState(
            targetValue = dataUsageRemember,
            animationSpec = tween(
                durationMillis = animationDuration
            )
        )
    
        // This is to start the animation when the activity is opened
        LaunchedEffect(Unit) {
            dataUsageRemember = dataUsage
        }
    
        Box(
            modifier = Modifier
                .size(size),
            contentAlignment = Alignment.Center
        ) {
            Canvas(
                modifier = Modifier
                    .size(size)
            ) {
                // For shadow
                drawCircle(
                    brush = Brush.radialGradient(
                        colors = listOf(shadowColor, Color.White),
                        center = Offset(x = this.size.width / 2, y = this.size.height / 2),
                        radius = this.size.height / 2
                    ),
                    radius = this.size.height / 2,
                    center = Offset(x = this.size.width / 2, y = this.size.height / 2)
                )
    
                // This is the white circle that appears on the top of the shadow circle
                drawCircle(
                    color = Color.White,
                    radius = (size / 2 - indicatorThickness).toPx(),
                    center = Offset(x = this.size.width / 2, y = this.size.height / 2)
                )
    
                // Convert the dataUsage to angle
                val sweepAngle = (dataUsageAnimate.value) * 360 / 100
    
                // Foreground indicator
                drawArc(
                    color = foregroundIndicatorColor,
                    startAngle = -90f,
                    sweepAngle = sweepAngle,
                    useCenter = false,
                    style = Stroke(width = indicatorThickness.toPx(), cap = StrokeCap.Round),
                    size = Size(
                        width = (size - indicatorThickness).toPx(),
                        height = (size - indicatorThickness).toPx()
                    ),
                    topLeft = Offset(
                        x = (indicatorThickness / 2).toPx(),
                        y = (indicatorThickness / 2).toPx()
                    )
                )
            }
    
            // Display the data usage value
            DisplayText(
                animateNumber = dataUsageAnimate,
                dataTextStyle = dataTextStyle,
                remainingTextStyle = remainingTextStyle
            )
        }
    
        Spacer(modifier = Modifier.height(32.dp))
    
        ButtonProgressbar {
            dataUsageRemember = (0 until 100).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
            )
        }
    }

    Related Articles:

    Leave a Comment