Jetpack Compose Gradient Progress Bar

jetpack compose custom progress indicator

This is a linear gradient progress bar made with Android Jetpack Compose. It has round corners. It uses Canvas API to draw the foreground and background bars. I have also added animation to the foreground bar.

The default Jetpack Compose LinearProgressIndicator doesn’t offer many customizations. You cannot set the round corners, height, animation duration, size, and colors. I have used the Canvas API to make this custom linear progress indicator. The composable takes parameters like height, color, padding, gradient color set, animation delay, and duration. You can customize them as per your requirements.

The progress will be updated automatically using viewModel. When you launch the activity, it starts a background thread inside the viewModel.launch block. The background thread updates the progress bar using the MutableLiveData variable. While changing the progress, the foreground bar moves forward with a smooth animation.

The Jetpack Compose progress bar has two bars. They are foreground and background. The background bar or indicator has a light grey color (grey color with 0.3f alpha) and the foreground bar has a gradient color. It gets animated every time progress is changed. I’m using animateFloatAsState API for animation and observeAsState API to update the progress bar. The foreground bar starts at the left and moves to the right by changing the position along the x-axis. I set the animation duration to 1 second and the delay to 0 seconds. But, you can change them according to your requirements.

The progress bar shows the progress in the form of text using the Text composable. I put it below the progress bar. Both the Canvas and Text are wrapped in a Column composable with alignment and arrangement equal to the center.

Make sure that you are using the latest Android Studio version before adding this code to your project. 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.

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.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
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
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext


/*
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 = MaterialTheme.colors.background),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    GradientProgressbar1()
                }
            }
        }
    }
}

@Composable
fun GradientProgressbar1(
    viewModel: MyViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
    indicatorHeight: Dp = 24.dp,
    backgroundIndicatorColor: Color = Color.LightGray.copy(alpha = 0.3f),
    indicatorPadding: Dp = 24.dp,
    gradientColors: List<Color> = listOf(
        Color(0xFF6ce0c4),
        Color(0xFF40c7e7),
        Color(0xFF6ce0c4),
        Color(0xFF40c7e7)
    ),
    numberStyle: TextStyle = TextStyle(
        fontFamily = FontFamily(Font(R.font.roboto_bold, FontWeight.Bold)),
        fontSize = 32.sp
    ),
    animationDuration: Int = 1000,
    animationDelay: Int = 0
) {
    val downloadedPercentage by viewModel.downloadedPercentage.observeAsState(initial = 0f)

    val animateNumber = animateFloatAsState(
        targetValue = downloadedPercentage,
        animationSpec = tween(
            durationMillis = animationDuration,
            delayMillis = animationDelay
        )
    )

    LaunchedEffect(Unit) {
        viewModel.startThreadGradient()
    }

    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .height(indicatorHeight)
            .padding(start = indicatorPadding, end = indicatorPadding)
    ) {

        // Background indicator
        drawLine(
            color = backgroundIndicatorColor,
            cap = StrokeCap.Round,
            strokeWidth = size.height,
            start = Offset(x = 0f, y = 0f),
            end = Offset(x = size.width, y = 0f)
        )

        // Convert the downloaded percentage into progress (width of foreground indicator)
        val progress =
            (animateNumber.value / 100) * size.width // size.width returns the width of the canvas

        // Foreground indicator
        drawLine(
            brush = Brush.linearGradient(
                colors = gradientColors
            ),
            cap = StrokeCap.Round,
            strokeWidth = size.height,
            start = Offset(x = 0f, y = 0f),
            end = Offset(x = progress, y = 0f)
        )

    }

    Spacer(modifier = Modifier.height(8.dp))

    Text(
        text = downloadedPercentage.toInt().toString() + "%",
        style = numberStyle
    )

}

class MyViewModel : ViewModel() {

    var downloadedPercentage = MutableLiveData<Float>()

    fun startThreadGradient() {
        viewModelScope.launch {
            withContext(Dispatchers.Default) {

                val totalDownloadSize = 1024f
                var downloadedSize = 0f

                while (true) {

                    downloadedSize += ((1..100).random().toFloat())

                    if (downloadedSize < totalDownloadSize) {
                        withContext(Dispatchers.Main) {
                            downloadedPercentage.value =
                                ((downloadedSize / totalDownloadSize) * 100)
                        }
                    } else {
                        withContext(Dispatchers.Main) {
                            downloadedPercentage.value = 100f
                        }
                        break
                    }

                    delay(1000)
                }

            }
        }
    }
}

Leave a Comment