Slider En Compose

En esta explicación verás el uso del componente Slider en Compose con el fin de permitirle a tus usuarios la selección de un rango de valores. Dichos rango es reflejado en una barra horizontal, donde un icono llamado palanca, especifica la selección actual.

Tipos de Sliders en Compose
Figura 1. Tipos de Sliders en Compose

Específicamente, abordaremos tres tipos de Slider: continuo, discreto y con doble selección. El objetivo es comprender el uso de las funciones componibles que los muestran en pantalla y los atributos asociados.

La siguiente tabla de contenido detalla el temario a estudiar:


Ejemplos De Slider En Compose

Encontrarás todos los ejemplos discutidos en el repositorio GitHub de mi guía de Jetpack Compose.

Solo navega hasta el paquete examples/Sliders del módulo :p7_componentes y ahí verás los archivos Kotlin para los ejemplos y la UI principal donde se despliegan todos.

Ejemplos de Slider En Compose

Slider Continuo

Slider Continuo En Compose
Figura 2. Slider continuo en Compose

Un Slider continuo permite al usuario establecer y seleccionar un elemento a partir de un rango subjetivo de valores.

La función componible que lo representa es Slider() y su declaración es la siguiente:

@Composable
fun Slider(
    value: Float!,
    onValueChange: ((Float) -> Unit)?,
    modifier: Modifier! = Modifier,
    enabled: Boolean! = true,
    valueRange: ClosedFloatingPointRange<Float!>! = 0f..1f,
    steps: Int! = 0,
    onValueChangeFinished: (() -> Unit)? = null,
    interactionSource: MutableInteractionSource! = remember { MutableInteractionSource() },
    colors: SliderColors! = SliderDefaults.colors()
): Unit

Donde cada parámetro es:

  • value: El valor actual seleccionado dentro del rango definido por valueRanged
  • onValueChange: Función del tipo (Float)->Unit que es invocada cuando el valor seleccionado cambia (producto de un evento de clic o de arrastre)
  • enabled: Determina si el Slider está habilitado, o no, para recibir eventos del usuario
  • valueRanged: Rango de valores flotantes que el Slider restringe para la selección
  • steps: Especifica la cantidad de segmentos que dividirán al rango establecido. Con este valor mayor a 0 creamos un Slider discreto
  • onValueChangeFinished: Función lambda invocada cuando el usuario confirma una selección
  • colors: Los colores usados en las partes del Slider. Crea una instancia SliderColors con la función SliderDefaults.colors()

Ejemplo: Slider Continuo Simple

Creemos nuestra primera función componible para mostrar en pantalla un Slider continuo en su forma más básica. Usemos un rango de 0 a 100 y establezcamos una selección del valor 50:

@Composable
fun SimpleContinuousSlider() {
    val range = 0f..100f
    var selection by remember { mutableStateOf(50f) }

    Slider(
        value = selection,
        valueRange = range,
        onValueChange = { selection = it }
    )
}

Es clave actualizar el atributo value del Slider en el parámetro onValueChange. Esto permitirá visualizar la selección en la UI. Claramente conservar este valor en recomposición se hace con el estado selection que hemos declarado.

SimpleContinuousSlider() en funcionamiento

Slider Discreto

Slider discreto en Compose
Figura 3. Slider discreto en Compose

Los Sliders discretos permiten al usuario seleccionar un valor específico del rango establecido. Dichos valores son visibles a través de marcas distribuidas por toda la pista como se muestra en la figura anterior.

Para crear uno, usamos el parámetro steps como vimos en la definición de Slider(). Con él determinamos el número de divisiones por un valor entero positivo.


Ejemplo: Slider Discreto Simple

Si tomamos el slider continuo del ejemplo 1 y le agregamos tres “pasos” entre sus valores mínimo y máximo, reproduciremos la imagen inicial:

@Composable
fun SimpleDiscreteSlider() {
    val range = 0.0f..100.0f
    val steps = 3
    var selection by remember { mutableStateOf(50f) }

    Slider(
        value = selection,
        valueRange = range,
        steps = steps,
        onValueChange = { selection = it }
    )
}

Al entrar en modo de interacción del panel de Compose en Android Studio, se visualizará su comportamiento:

SimpleDiscreteSlider() en funcionamiento

Slider Con Doble Selección

Slider con doble selección
Figura 4. Slider con doble selección (Range Slider)

En caso de que necesites dos palancas de selección para acotar un subrango en el Slider, usa el componente RangeSlider() para materializar este propósito.

@Composable
@ExperimentalMaterialApi
fun RangeSlider(
    values: ClosedFloatingPointRange<Float!>!,
    onValueChange: ((ClosedFloatingPointRange<Float>) -> Unit)?,
    modifier: Modifier! = Modifier,
    enabled: Boolean! = true,
    valueRange: ClosedFloatingPointRange<Float!>! = 0f..1f,
    steps: Int! = 0,
    onValueChangeFinished: (() -> Unit)? = null,
    colors: SliderColors! = SliderDefaults.colors()
): Unit

Aunque la mayoría de parámetros se conservan, el soporte de doble selección se fundamenta en:

  • values: Un rango flotante que especifica la selección del valor mínimo y máximo
  • onValueChange: Debido a que value ahora es un rango flotante, entonces esta lambda cambia su parámetro de Float a ClosedFloatingPointRange

Nota: En el momento que escribo este tutorial, RangeSlider aún es experimental.


Ejemplo: Slider Con Doble Selección Simple

Supongamos que tenemos una App de comercio electrónico que posee una pantalla para filtrar los productos del catálogo. Entre todos los controles que usa, se requiere un Slider discreto para seleccionar aquellos en un rango de precios como en la figura anterior.

Su rango general es entre 1 y 1000 dólares; y habrá cinco marcas para crear los subrangos [1, 200], [200,400], [400,600], [600,800] y [800, 1000].

La solución correspondiente con RangeSlider() es:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SimpleRangeSlider() {
    val range = 1.0f..1000.0f
    val steps = 4
    var price by remember { mutableStateOf(200.0f..400.0f) }

    RangeSlider(
        values = price,
        valueRange = range,
        steps = steps,
        onValueChange = { price = it }
    )
}

Si te fijas, esta vez el estado price que almacena la selección es de tipo ClosedFloatingPointRange<Float>. De esta forma conservaremos la doble selección en cada repintado.

Ejemplo RangeSlider()
SimpleRangeSlider() en funcionamiento

Hasta aquí hemos visto los tres tipos de sliders en su forma básica. A continuación veremos algunos casos de aplicación para mejorar la comunicación de los ejemplos previos.


Añadir Texto Con Valor Seleccionado

Figura 5. Ejemplo de Slider con texto

Tomemos el primer ejemplo y añadamos un componente de texto que muestre el valor actual seleccionado del Slider. Este representará el cambio de tamaño de fuente del texto «Develou» en el rango [25, 50].

¿En qué consiste la solución?

Es simple:

  1. Modificar el atributo fontSize del texto superior usando la propiedad de extensión sp del estado
  2. En cada gesto que cambie la selección visualmente (onValueChange), actualizar el parámetro value del Slider
  3. Actualizar el parámetro text del Text() con el estado del Slider. Este es alineado a la derecha del Slider con un layout Row():
@Composable
fun ContinuousSliderWithValue() {
    val range = 25f..50f
    var fontSize by remember { mutableStateOf(30f) }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {

        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier.height(100.dp)
        ) {

            Text(text = "Develou", fontSize = fontSize.sp) // (1)
        }

        Spacer(Modifier.height(8.dp))

        Row(verticalAlignment = Alignment.CenterVertically) {

            Slider(
                value = fontSize,
                valueRange = range,
                onValueChange = { fontSize = it }, // (2)
                modifier = Modifier
                    .weight(0.9f)
                    .padding(end = 16.dp)
            )
            Text(
                text = fontSize.toInt().toString(), // (3)
                modifier = Modifier.weight(0.1f)
            )
        }
    }

}

El resultado al previsualizar es:

Ejemplo Slider con texto en Compose
ContinuousSliderWithValue() en funcionamiento

Modificar Valor Seleccionado Por Campo De Texto

Slider con TextField
Figura 5. Slider con TextField

Ahora permitamos al usuario que modifique el valor del Slider discreto del segundo ejemplo a partir de un campo de texto.

¿Cómo solucionarlo?

Este escenario es similar en organización al ejemplo anterior, solo que reemplazamos al componente Text() por uno TextField().

Cuando el usuario modifique el texto del campo, entonces actualizaremos el estado de selección del Slider. En código esto es:

@Composable
fun DiscreteSliderWithTextField() {
    val range = 0f..100f
    val steps = 4
    var sliderSelection by remember { mutableStateOf(range.start) }
    var selectionNumber by remember { mutableStateOf(range.start.toInt().toString()) }

    Row {

        Slider(
            value = sliderSelection,
            valueRange = range,
            steps = steps,
            onValueChange = { sliderSelection = it },
            onValueChangeFinished = {
                selectionNumber = sliderSelection.toInt().toString() // (1)
            },
            modifier = Modifier.width(250.dp)
        )
        
        Spacer(Modifier.width(16.dp))
        
        TextField(
            value = selectionNumber,
            onValueChange = {
                val segment = calculateSegment(it, range, steps) // (2)
                sliderSelection = segment
                selectionNumber = it
            },
            singleLine = true,
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
            shape = RoundedCornerShape(4.dp),
            modifier = Modifier.width(56.dp),
            colors = TextFieldDefaults.textFieldColors(unfocusedIndicatorColor = Color.Transparent)
        )
    }
}

Puntos a tener en cuenta:

  1. Actualizamos el estado del texto sólo cuando el usuario confirma el cambio de la selección en el slider
  2. Debido a que el campo de texto puede recibir valores diferentes al rango, creamos la función calculateSegment() para elegir el segmento asociado en el Slider
  3. Si lo deseas, puedes optar por realizar la corrección del TextField desde su acción IME de confirmación

En el caso de calculateSegment(), el segmento a que pertenece es el producto entre el tamaño de los subrangos por la representación porcentual de la selección actual:

fun calculateSegment(input: String, range: ClosedFloatingPointRange<Float>, steps: Int): Float {
    if (input.isBlank()) return 0.0F

    val selection = input.toFloat()

    if (selection > range.endInclusive) return range.endInclusive

    val segments = steps + 1
    val subRangeSize = (range.endInclusive - range.start) / segments

    val fraction: Float = range.endInclusive / selection
    val location = (segments / fraction).roundToInt()

    return location * subRangeSize
}

Si corres la App verás:

Ejemplo Slider con TextField Compose
DiscreteSliderWithTextField() en funcionamiento

Cambiar Color Del Slider

Ejemplo de Slider coloreado
Figura 7. Ejemplo de Slider coloreado

Por último, finalicemos cambiando el color de las partes del Slider en el ejemplo 3. La idea es asignar Verde 500 a la pista (track), Verde 900 a la palanca (thumb), Lima 500 a las marcas (ticks) activas y Gris claro para ticks inactivas.

Ya sabemos que el parámetro colors de Slider o RangeSlider define el color aplicado a cada parte del componente, por lo que solo resta crear una instancia de SliderColor. Esto se logra con la función SliderDefaults.colors():

@Composable
fun colors(
    thumbColor: Color! = MaterialTheme.colors.primary,
    disabledThumbColor: Color! = MaterialTheme.colors.onSurface
            .copy(alpha = ContentAlpha.disabled)
            .compositeOver(MaterialTheme.colors.surface),
    activeTrackColor: Color! = MaterialTheme.colors.primary,
    inactiveTrackColor: Color! = activeTrackColor.copy(alpha = InactiveTrackAlpha),
    disabledActiveTrackColor: Color! = MaterialTheme.colors.onSurface.copy(alpha = DisabledActiveTrackAlpha),
    disabledInactiveTrackColor: Color! = disabledActiveTrackColor.copy(alpha = DisabledInactiveTrackAlpha),
    activeTickColor: Color! = contentColorFor(activeTrackColor).copy(alpha = TickAlpha),
    inactiveTickColor: Color! = activeTrackColor.copy(alpha = TickAlpha),
    disabledActiveTickColor: Color! = activeTickColor.copy(alpha = DisabledTickAlpha),
    disabledInactiveTickColor: Color! = disabledInactiveTrackColor
            .copy(alpha = DisabledTickAlpha)
): SliderColors

Y como ves, existe un atributo para cada una de las partes mencionadas: *thumbColor, *TrackColor y *TickColor. Hay una variación diferente para estado (disabled) y situación según la selección (active y inactive).

¿Cuál es la solución?

Nada más que declarar los colores que usaremos para crear la instancia SliderColor y pasarlo al Slider. Veamos:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ColoredSlider() {
    val range = 1.0f..1000.0f
    val steps = 4
    var price by remember { mutableStateOf(1f..200f) }

    val green500 = Color(0xFF4CAF50)
    val green900 = Color(0xFF1B5E20)
    val lime500 = Color(0xFFCDDC39)

    RangeSlider(
        values = price,
        valueRange = range,
        steps = steps,
        onValueChange = { price = it },
        colors = SliderDefaults.colors(
            thumbColor = green900,
            activeTrackColor = green500,
            inactiveTrackColor = Color.LightGray.copy(alpha = 0.24f),
            activeTickColor = lime500,
            inactiveTickColor = lime500.copy(alpha = 0.56f)
        )
    )
}

Al ejecutar el aplicativo, nuestro slider de precios estará más radiante:

Ú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!