Icono del sitio Develou

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.

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.


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:

Veamos algunos ejemplos.


1.1 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

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

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:


2.1 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

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

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:

Salir de la versión móvil