Material Slider

Google lanzó Material Components 1.3.0-alpha02 el 16 de julio de 2020. Un componente que ha recibido mucho amor en esta versión es el humilde deslizador. Hay algunas buenas adiciones en la parte superior de la funcionalidad básica, y en este post vamos a explorar un poco.

Fuente de la imagen
Licencia bajo CC 2.0

Un Slider es un control útil para permitir al usuario especificar valores sin tener que usar el teclado. Su uso básico es muy similar al de los widgets SeekBar o AppCompatSeekBar del Android Framework. Para estos hay varios usos: como desplazarse a un lugar específico durante la reproducción de audio o vídeo; o especificar un valor dentro de un rango determinado. Sin embargo, hay algunos casos de uso adicionales que Slider nos proporciona, como veremos más adelante en este artículo

Empezaremos por crear un control deslizante muy sencillo y ver cómo se ve y se comporta.

Comportamiento básico

Crear un Material Slider es muy sencillo:

res/layout/activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

<?xml version=»1.0″ encoding=»utf-8″?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=»http://schemas.android.com/apk/res/android»
xmlns:app=»http://schemas.android.com/apk/res-auto»
xmlns:tools=»http://schemas.android.com/tools»
android:layout_width=»match_parent»
android:layout_height=»match_parent»
android:padding=»32dp»
tools:context=».MainActivity»>
<androidx.appcompat.widget.AppCompatSeekBar
android:id=»@+id/seek_bar»
android:layout_width=»0dp»
android:layout_height=»wrap_content»
android:valueFrom=»0″
android:valueTo=»100″
app:layout_constraintBottom_toTopOf=»@id/discrete_slider»
app:layout_constraintEnd_toEndOf=»parent»
app:layout_constraintStart_toStartOf=»parent»
app:layout_constraintTop_toTopOf=»parent» />
<com.google.android.material.slider.Slider
android:id=»@+id/continuous_slider»
android:layout_width=»0dp»
android:layout_height=»wrap_content»
android:valueFrom=»0″
android:valueTo=»100″
app:layout_constraintBottom_toTopOf=»@id/discrete_slider»
app:layout_constraintEnd_toEndOf=»@id/seek_bar»
app:layout_constraintStart_toStartOf=»@id/seek_bar»
app:layout_constraintTop_toBottomOf=»@id/seek_bar» />
</ConstraintLayout>

Especificamos un rango de 0-100 en ambos controles utilizando los atributos valueFrom y valueTo. Haciendo esto nos da los siguientes comportamientos (AppCompatSeekBar está en la parte superior, y Slider está en la parte inferior):

Aunque estos parecen muy similares hay algunas diferencias importantes. En primer lugar, el Slider está diseñado con los colores definidos en mi tema, mientras que el AppCompatSeekBar utiliza los colores por defecto de la biblioteca AppCompat. En segundo lugar, el Slider es ligeramente más grande, lo que lo hace un poco más claro para el usuario. Probablemente la diferencia más importante es la adición de una etiqueta al Slider cuando el usuario arrastra la posición. Esto hace que sea más fácil para el usuario entender el valor preciso que está seleccionando. Si bien es cierto que podemos añadir una Vista separada a nuestro diseño para mostrar este valor, la etiqueta de información sobre herramientas hace que la interfaz de usuario esté menos desordenada. Podemos desactivar esto si no es necesario. Añadir app:labelBehaviour="gone" logrará esto.

Sin embargo, hay otros dos comportamientos de la etiqueta. El primero es el predeterminado que podemos ver en el ejemplo anterior – esto es app:labelBehaviour="floating" que flota la etiqueta sobre la vista. La otra opción es app:labelBehaviour="withinBounds". Esta opción se comporta igual que floating pero la diferencia es cómo afecta al tamaño de la etiqueta Slider. Con floating la altura medida del control no incluye la etiqueta, mientras que withinBounds sí incluye la etiqueta en la altura medida. Esto puede ser importante dependiendo de si queremos asegurarnos de que la etiqueta no oculta ningún otro control dentro de nuestro diseño.

Deslizador discreto

La variante de Slider que hemos visto hasta ahora se conoce como Deslizador continuo. Esto significa que el valor que devuelve puede ser cualquier valor dentro del rango. Sin embargo, hay casos en los que podemos querer limitar esto. Por ejemplo, los valores fraccionarios son devueltos por este Slider continuo, pero podemos requerir que el valor se limite a enteros o incluso a pasos mayores. Para lograr esto podemos utilizar un deslizador discreto – que devuelve un conjunto discreto de valores. Podemos cambiar un Slider continuo a uno discreto añadiendo un único atributo:

1
2
3
4
5
6
7
8
9
10
11

<com.google.android.material.slider.Slider
android:id=»@+id/continuous_slider»
android:layout_width=»0dp»
android:layout_height=»wrap_content»
android:stepSize=»10″
android:valueFrom=»0″
android:valueTo=»100″
app:layout_constraintBottom_toTopOf=»@id/discrete_slider»
app:layout_constraintEnd_toEndOf=»@id/seek_bar»
app:layout_constraintStart_toStartOf=»@id/seek_bar»
app:layout_constraintTop_toBottomOf=»@id/seek_bar» />

Añadir app:stepSize="10" hace que el Slider sólo devuelva valores que sean múltiplos de 10. Esto cambia ligeramente la UX:

En primer lugar, hay marcas que indican los valores discretos que se devolverán. Estas son bastante sutiles, pero se pueden ver claramente en la pista. La segunda diferencia es que la selección ya no es suave, sino que salta entre los valores discretos como podemos ver por el valor de la etiqueta. El usuario tiene una clara indicación de que la selección es de un conjunto discreto de valores.

RangeSlider

También podemos utilizar otra variante de Slider. Hasta ahora hemos visto cómo seleccionar un único valor utilizando un Slider, pero el usuario puede seleccionar un rango utilizando un RangeSlider.

Cuando configuramos por primera vez Slider utilizamos valueFrom y valueTo para especificar ese rango de valores que devuelve el deslizador.Un RangeSlider permite al usuario especificar un rango. Por ejemplo: si estuviéramos proporcionando una facilidad de búsqueda dentro de una app de compras, podríamos querer que el usuario pudiera especificar un rango de precios. Podemos conseguirlo utilizando un RangeSlider que admite los modos continuo y discreto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

<com.google.android.material.slider.RangeSlider
android:id=»@+id/continuous_range»
android:layout_width=»0dp»
android:layout_height=»wrap_content»
android:valueFrom=»0″
android:valueTo=»100″
app:layout_constraintBottom_toTopOf=»@id/discrete_range»
app:layout_constraintEnd_toEndOf=»@id/continuous_slider»
app:layout_constraintStart_toStartOf=»@id/continuous_slider»
app:layout_constraintTop_toBottomOf=»@id/discrete_slider»
app:values=»@array/double_slider_values»/>
<com.google.android.material.slider.RangeSlider
android:id=»@+id/discrete_range»
android:layout_width=»0dp»
android:layout_height=»wrap_content»
android:stepSize=»10″
android:valueFrom=»0″
android:valueTo=»100″
app:layout_constraintBottom_toBottomOf=»parent»
app:layout_constraintEnd_toEndOf=»@id/continuous_slider»
app:layout_constraintStart_toStartOf=»@id/continuous_slider»
app:layout_constraintTop_toBottomOf=»@id/continuous_range»
app:values=»@array/triple_slider_values» />

Aquí hay un atributo obligatorio: app:values – sin esto RangeSlider vuelve al comportamiento estándar de Slider. Debemos suministrar un array de valores en este atributo:

res/values/arrays.xml

1
2
3
4
5
6
7
8
9
10
11
12
13

<?xml version=»1.0″ encoding=»utf-8″?>
<recursos>
<array name=»double_slider_values»>
<item>20</item>
<item>60</item>
</array>
<array name=»triple_slider_values»>
<elemento>20</elemento>
<elemento>60</elemento>
<elemento>100</elemento>
</array>
</resources>

Estas matrices tienen dos y tres valores respectivamente, y el continuo RangeSlider obtiene el rango con dos valores mientras que el discreto RangeSlider obtiene el rango con tres valores. Esto nos da algunos comportamientos nuevos e interesantes:

El ejemplo superior muestra un rango simple de dos valores que permite al usuario especificar un rango con límites superiores e inferiores. El rango global que establecemos para el RangeSlider restringe los valores máximos y mínimos que el usuario puede especificar.

El ejemplo inferior es aún más interesante. Además de ser un RangeSlider discreto, los tres valores de la matriz crean tres puntos de pulgar en el control. Además de un límite superior e inferior del rango, el usuario también puede especificar un punto en algún lugar dentro de los límites superior e inferior. El usuario no puede arrastrar el punto central del pulgar fuera de los límites establecidos por los puntos superior e inferior. La etiqueta del punto del pulgar que se arrastra siempre se dibuja encima de las otras etiquetas que podría solapar. Esto es importante para que el usuario sepa el valor que está seleccionando actualmente.

Podemos recuperar los valores seleccionados llamando al método getValues() de RangeSlider. Esto devuelve una matriz de valores cuyo tamaño es el mismo que el tamaño de la matriz que especificamos en app:values.

Formato de la etiqueta

Los más observadores pueden haber notado que hay una diferencia en el formato de la etiqueta de los valores dobles y triples RangeSliders. El valor doble continuo RangeSlider utiliza el formato de etiqueta por defecto mientras que el valor triple discreto tiene el prefijo Value:. Esto nos lleva al hecho de que podemos personalizar la etiqueta:

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.discreteRange.setLabelFormatter {
getString(R.string.label_format, it)
}
}
}

LabelFormatter es una interfaz SAM con un único método getFormattedValue() que se ha convertido en un lambda en este caso. Para ello he utilizado un recurso de cadena de formato:

res/values/strings.xml

1
2
3
4

<recursos>
<string name=»nombre_de_la_aplicación»>Deslizador de material</string>
<string name=»formato_de_la_etiqueta»>Valor: %1$.0f</string>
</resources>

Hay que hacerlo de forma programada. Estoy bastante seguro de que una cadena de formato como esta se ajustaría a la mayoría de los casos de uso, por lo que estaría bien que pudiéramos especificar una mediante un atributo XML. Sin embargo, aplicar uno programáticamente es bastante trivial.

Conclusión

Slider y RangeSlider son iteraciones realmente agradables en SeekBar. Proporcionan la misma funcionalidad y mucho más. Estoy particularmente impresionado por la atención a los detalles – como dibujar la etiqueta activa encima de las pasivas. Este es un control bien diseñado e implementado, y tengo toda la intención de utilizarlo!