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.
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
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 porvalueRanged
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 usuariovalueRanged
: Rango de valores flotantes que el Slider restringe para la selecciónsteps
: Especifica la cantidad de segmentos que dividirán al rango establecido. Con este valor mayor a0
creamos un Slider discretoonValueChangeFinished
: Función lambda invocada cuando el usuario confirma una seleccióncolors
: Los colores usados en las partes del Slider. Crea una instanciaSliderColors
con la funciónSliderDefaults.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.
Slider Discreto
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:
Slider Con Doble Selección
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.
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
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:
- Modificar el atributo fontSize del texto superior usando la propiedad de extensión sp del estado
- En cada gesto que cambie la selección visualmente (
onValueChange
), actualizar el parámetro value del Slider - Actualizar el parámetro
text
delText()
con el estado del Slider. Este es alineado a la derecha del Slider con un layoutRow()
:
@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:
Modificar Valor Seleccionado Por Campo De Texto
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:
- Actualizamos el estado del texto sólo cuando el usuario confirma el cambio de la selección en el slider
- 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 - 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:
Cambiar Color Del Slider
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: