Icono del sitio Develou

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.

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.


Slider Continuo

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:


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

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

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:

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.

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:

ContinuousSliderWithValue() en funcionamiento

Modificar Valor Seleccionado Por Campo De Texto

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:

DiscreteSliderWithTextField() en funcionamiento

Cambiar Color Del Slider

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:

Salir de la versión móvil