Indicadores De Progreso En Compose

En este tutorial estudiaremos los indicadores de progreso en Compose, para representar el inicio de una tarea que tomará un tiempo indefinido, o bien, una duración con el porcentaje avanzado.

Tipos de indicadores de progreso
Figura 1. Tipos de indicadores de progreso

Encontrarás varios ejemplos sobre la creación de indicadores lineales y circulares como se muestra en la anterior imagen. Dirígete a ellos por la siguiente lista de contenidos:


Indicadores De Progreso En Compose

Al igual que los demás apartados de la guía de Jetpack Compose, el código para los indicadores de progreso está disponible en el repositorio de GitHub:

Localiza las funciones componibles en el paquete examples/ProgressIndicadors del módulo :p7_componentes. Recuerda apoyar el repositorio con un Star.

Ejemplo de indicadores de progreso en Compose

1. Indicador De Progreso Lineal

Para crear una indicador de progreso lineal usa la función componible LinearProgressIndicador() en cualquiera de sus dos versiones (determinado e indeterminado):

// Determinado
@Composable
fun LinearProgressIndicator(
    progress: Float?,
    modifier: Modifier? = Modifier,
    color: Color? = MaterialTheme.colors.primary,
    backgroundColor: Color? = color.copy(alpha = IndicatorBackgroundOpacity)
): Unit

// Inderterminado
@Composable
fun LinearProgressIndicator(
    modifier: Modifier? = Modifier,
    color: Color? = MaterialTheme.colors.primary,
    backgroundColor: Color? = color.copy(alpha = IndicatorBackgroundOpacity)
): Unit

Donde sus parámetros representan los siguientes atributos:

  • progress: La cantidad de progreso del indicador. Su rango de valores es [0.0, 1.0]
  • color: Color del indicador de progreso
  • backgroundColor: Color de la pista del indicador

Veamos algunos ejemplos.


1.1 LinearProgressIndicator Determinado

LinearProgressIndicator determinado
Figura 2. LinearProgressIndicator determinado

Supongamos que deseamos representar el proceso de sincronización de datos del usuario, a través de un indicador lineal como se muestra en la figura 2.

En principio solo debemos declarar el estado inicial y pasarlo al atributo progress del componente:

@Composable
fun DeterminedLinearProgress() {
    var progress by remember { mutableStateOf(0.0f) }


    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.padding(all = 16.dp)
    ) {
        Text(text = "Sincronizando datos")

        Spacer(Modifier.height(16.dp))

        LinearProgressIndicator(
            progress = animatedProgress,
            modifier = Modifier
                .wrapContentHeight()
                .padding(horizontal = 16.dp)
        )
    }
}

Ahora bien, si queremos simular el avance del progreso, podemos ejecutar una corrutina con LaunchedEffect. Esta se encargará de incrementar el estado del progreso en 10 puntos porcentuales en en intervalos de 300 milisegundos:

@Composable
fun DeterminedLinearProgress() {
    var progress by remember { mutableStateOf(0.0f) }

    LaunchedEffect(true) { // Incrementos

        for (i in 0..100 step 10) {
            delay(300)

            if (i == 100) {
                cancel()
            }

            progress = i / 100f
        }
    }

    val animatedProgress by animateFloatAsState( // Animación de progreso
        targetValue = progress,
        animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
    )

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.padding(all = 16.dp)
    ) {
        Text(text = "Sincronizando datos")

        Spacer(Modifier.height(16.dp))

        LinearProgressIndicator(
            progress = animatedProgress,
            modifier = Modifier
                .wrapContentHeight()
                .padding(horizontal = 16.dp)
        )
    }
}

Adicionalmente, animamos el progreso entre cada valor con animateFloatAsState() con el fin de visualizar un avance más fluido.

De esta forma, al ejecutar o entrar al modo interacción, verás la siguiente animación del progreso:


1.2 LinearProgressIndicator Indeterminado

LinearProgressIndicator indeterminado
Figura 3. LinearProgressIndicator indeterminado

Tomemos como ejemplo un escaneo sobre la cantidad de medidores de electricidad que existen en un sector. Debido a que la cantidad varía de sector a sector, el progreso será indefinido como se muestra en la figura 3.

A fin de implementar este indicador de progreso lineal indeterminado, invocamos a la función sin el parámetro progress:

@Composable
fun UndeterminedLinearProgress() {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.padding(all = 16.dp)
    ) {
        Text(text = "Escaneando medidores")

        Spacer(Modifier.height(16.dp))

        LinearProgressIndicator()
    }
}

Al previsualizar la función UndeterminedLinearProgress() verás el indicador animado entre la pista hasta que se llegue al fin de la tarea:


1.3 Cambiar Color Del Indicador De Progreso

LinearProgressIndicator de color Azul
Figura 4. LinearProgressIndicator de color Azul

Usa los parámetros color y backgroundColor para modificar el color del indicador y la pista respectivamente.

Por ejemplo, usemos Azul 500 para el progreso y el gris claro Color.LightGray en un indicador lineal indeterminado:

@Composable
fun ColoredLinearProgress() {
    val blue500 = Color(0xFF2196F3)
    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier.padding(16.dp)
    ) {
        LinearProgressIndicator(
            color = blue500,
            backgroundColor = Color.LightGray
        )
    }
}

El efecto del cambio de color es:


2. Indicador De Progreso Circular

En el caso de los indicadores circulares, invoca a la función CircularProgressIndicador() para crear y mostrarlos en pantalla. Al igual que los lineares, esta función viene en dos versiones para determinados e indeterminados:

// Determinado
@Composable
fun CircularProgressIndicator(
    progress: Float?,
    modifier: Modifier? = Modifier,
    color: Color? = MaterialTheme.colors.primary,
    strokeWidth: Dp? = ProgressIndicatorDefaults.StrokeWidth
): Unit

// Indeterminado
@Composable
fun CircularProgressIndicator(
    modifier: Modifier? = Modifier,
    color: Color? = MaterialTheme.colors.primary,
    strokeWidth: Dp? = ProgressIndicatorDefaults.StrokeWidth
): Unit

Donde:

  • progress: Es el progreso que representa el recorrido circular con valores [0.0,1.0] (representación de 0 a 360 grados)
  • color: Es el color del indicador
  • strokeWidth: Es el ancho del indicador

2.1 CircularProgressIndicator Determinado

CircularProgressIndicator determinado
Figura 5. CircularProgressIndicator determinado

Mostremos en pantalla un indicador circular determinado cuyo fin es reflejar el estado de la subida de un archivo a la nube, como se ve en la imagen anterior.

La solución es exactamente igual a como vimos en el indicador de progreso lineal determinado. Ejecutamos una corrutina y añadimos la animación entre valores de progreso:

@Composable
fun DeterminedCircularProgress(progress: Float) {
    val percentage: Int = (progress * 100).toInt()

    Box(contentAlignment = Alignment.Center) {
        Text(text = "$percentage%", style = MaterialTheme.typography.body2)
        CircularProgressIndicator(
            progress = progress,
            modifier = Modifier.size(70.dp)
        )
    }
}

@Composable
fun UploadFileView() {
    var progress by remember { mutableStateOf(0.0f) }

    LaunchedEffect(true) {

        for (i in 0..100 step 10) {
            delay(300)

            if (i == 100) {
                cancel()
            }

            progress = i / 100f
        }
    }

    val animatedProgress by animateFloatAsState(
        targetValue = progress,
        animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
    )

    Column(
        Modifier
            .padding(all = 16.dp)
            .fillMaxSize()
            .wrapContentHeight()
            .border(width = 1.dp, color = Color.LightGray, shape = RoundedCornerShape(4.dp))
            .padding(all = 16.dp),
        horizontalAlignment = Alignment.CenterHorizontally

    ) {
        DeterminedCircularProgress(progress = animatedProgress)

        Spacer(Modifier.height(16.dp))

        Text(text = "Subiendo archivo...")

        Spacer(Modifier.height(8.dp))

        OutlinedButton(onClick = { }) {
            Text(text = "Cancelar")
        }
    }
}

Hemos elevado el estado del indicador de progreso al crear la función DeterminedCircularProgress(), la cual recibe el valor del progreso. Esta es llamada desde UploadFileView() donde se crea el diseño junto a un texto y botón de cancelar.

El resultado final es:


2.2 CircularProgressIndicator Indeterminado

CircularProgressIndicator indeterminado
Figura 6. CircularProgressIndicator indeterminado

Al igual que el indicador lineal indeterminado, el circular no recibe parámetro progress. Basta con invocar a CircularProgressIndicator sin más.

@Composable
fun UndeterminedCircularProgress() {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
    ) {
        CircularProgressIndicator()

        Spacer(Modifier.height(16.dp))

        Text(text = "Cargando")
    }
}

Al materializarlo tenemos:


2.3 Cambiar Color Del Indicador De Progreso

CircularProgressIndicator con diferente color
Figura 7. CircularProgressIndicator con diferente color

Ya para finalizar, cambiemos el color del indicador circular anterior por amarillo y azul. La idea es crear una animación (todo) que intercambie cada 200 milisegundos entre estos dos valores.

El código de solución es:

@Composable
fun ColoredCircularIndicator() {
    val infiniteTransition = rememberInfiniteTransition()
    val color by infiniteTransition.animateColor(
        initialValue = Color.Blue,
        targetValue = Color.Yellow,
        animationSpec = infiniteRepeatable(
            animation = tween(200, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        )
    )

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
    ) {
        CircularProgressIndicator(color = color)

        Spacer(Modifier.height(16.dp))

        Text(text = "Cargando")
    }
}

Como ves, creamos una transición infinita entre Color.Blue y Color.Yellow por medio de animateColor(). Luego aplicamos el valor del estado color sobre el parámetro del CircularProgressIndicator.

El resultado de la animación es:

CircularProgressIndicator con infinityTransition

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!