Material Slider

Google wydało Material Components 1.3.0-alpha02 16 lipca 2020 roku. Jednym z komponentów, który otrzymał wiele miłości w tym wydaniu jest skromny suwak. Oprócz podstawowej funkcjonalności jest kilka miłych dodatków, a w tym poście trochę je poznamy.

Image Source
Licensed under CC 2.0

A Sliderjest użyteczną kontrolką pozwalającą użytkownikowi na określenie wartości bez konieczności używania klawiatury. Podstawowy przypadek użycia jest bardzo podobny do tego z Android Framework SeekBar lub widżetów AppCompatSeekBar. Dla nich istnieją różne zastosowania: takie jak przeskakiwanie do określonego miejsca podczas odtwarzania audio lub wideo; lub określanie wartości w danym zakresie. Istnieje jednak kilka dodatkowych przypadków użycia, które zapewnia nam Slider, jak zobaczymy w dalszej części tego artykułu

Zaczniemy od stworzenia bardzo prostej kontrolki suwaka i zobaczymy, jak wygląda i zachowuje się.

Podstawowe zachowanie

Tworzenie Material Slider jest bardzo proste:

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>

Określamy zakres 0-100 na obu kontrolkach za pomocą atrybutów valueFrom i valueTo. W ten sposób uzyskujemy następujące zachowania (AppCompatSeekBar jest na górze, a Slider na dole):

Pomimo że wyglądają one bardzo podobnie, istnieje kilka istotnych różnic. Po pierwsze, Slider jest stylizowany przy użyciu kolorów zdefiniowanych w moim motywie, podczas gdy AppCompatSeekBar używa domyślnych kolorów z biblioteki AppCompat. Po drugie, Slider jest nieco większy, dzięki czemu jest nieco bardziej przejrzysty dla użytkownika. Prawdopodobnie najważniejszą różnicą jest dodanie etykiety do Slider, gdy użytkownik przeciąga pozycję. Ułatwia to użytkownikowi zrozumienie dokładnej wartości, którą wybiera. Podczas gdy możemy oczywiście dodać osobny widok do naszego layoutu, aby wyświetlić tę wartość, etykieta tooltip sprawia, że UI jest mniej zagracone. Możemy to wyłączyć, jeśli nie jest to wymagane. Dodanie app:labelBehaviour="gone" pozwoli nam to osiągnąć.

Jednakże istnieją dwa inne zachowania etykiety. Pierwszym z nich jest domyślne, które możemy zobaczyć w poprzednim przykładzie – jest to app:labelBehaviour="floating", które unosi etykietę nad widokiem. Drugą opcją jest app:labelBehaviour="withinBounds". Ta opcja renderuje się tak samo jak floating, ale różnica polega na tym, jak wpływa na rozmiar Slider. W przypadku floating zmierzona wysokość kontrolki nie zawiera etykiety, podczas gdy withinBounds zawiera etykietę w zmierzonej wysokości. Może to być ważne w zależności od tego, czy chcemy się upewnić, że etykieta nie zasłania żadnych innych kontrolek w naszym układzie.

Suwak dyskretny

Odmiana Slider, której przyjrzeliśmy się do tej pory, jest znana jako suwak ciągły. Oznacza to, że zwracana przez niego wartość może być dowolną wartością z zakresu. Istnieją jednak przypadki, w których możemy chcieć to ograniczyć. Na przykład, wartości ułamkowe są zwracane przez ten ciągły Slider, ale możemy wymagać, aby wartość była ograniczona do liczb całkowitych lub nawet większych kroków. Aby to osiągnąć możemy użyć suwaka dyskretnego – który zwraca dyskretny zestaw wartości. Możemy zmienić ciągły Slider na dyskretny poprzez dodanie jednego atrybutu:

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” />

Dodanie app:stepSize="10" sprawia, że Slider zwraca tylko wartości, które są wielokrotnością 10. Zmienia to nieco UX:

Po pierwsze pojawiają się znaczniki, które wskazują dyskretne wartości, które zostaną zwrócone. Są one dość subtelne, ale widać je wyraźnie na ścieżce. Drugą różnicą jest to, że wybór nie jest już płynny, ale przeskakuje pomiędzy dyskretnymi wartościami, co możemy zobaczyć po wartości etykiety. Użytkownik ma wyraźne wskazanie, że wybór jest z dyskretnego zbioru wartości.

RangeSlider

Możemy również użyć innego wariantu Slider. Do tej pory widzieliśmy, jak wybrać pojedynczą wartość za pomocą Slider, ale użytkownik może wybrać zakres za pomocą RangeSlider.

Gdy po raz pierwszy skonfigurowaliśmy Slider, użyliśmy valueFrom i valueTo, aby określić zakres wartości zwracanych przez suwak. a RangeSlider pozwala użytkownikowi określić zakres. Na przykład: gdybyśmy udostępniali funkcję wyszukiwania w aplikacji do robienia zakupów, moglibyśmy chcieć, aby użytkownik mógł określić zakres cen. Możemy to osiągnąć za pomocą RangeSlider, który obsługuje zarówno tryb ciągły, jak i dyskretny:

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” />

Tutaj jest wymagany atrybut: app:values – bez tego RangeSlider spada z powrotem do standardowego zachowania Slider. Musimy dostarczyć tablicę wartości w tym atrybucie:

res/values/arrays.xml

1
2
3
4
5
6

.

7
8
9
10
11
12
13

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

Tablice te mają odpowiednio dwie i trzy wartości, i ciągły RangeSlider dostaje zakres z dwoma wartościami, podczas gdy dyskretny RangeSlider dostaje zakres z trzema wartościami. To daje nam kilka interesujących nowych zachowań:

Górny przykład pokazuje prosty dwuwartościowy zakres, który pozwala użytkownikowi na określenie zakresu z górną i dolną granicą. Ogólny zakres, który ustawiliśmy dla RangeSlider ogranicza wartości maksymalne i minimalne, które użytkownik może określić.

Niższy przykład jest jeszcze bardziej interesujący. Oprócz tego, że jest to dyskretny RangeSlider, trzy wartości w tablicy tworzą trzy punkty kciuka w kontrolce. Oprócz górnej i dolnej granicy zakresu, użytkownik może również określić punkt gdzieś w obrębie górnej i dolnej granicy. Użytkownik nie może przeciągnąć środkowego punktu kciuka poza granice wyznaczone przez górny i dolny punkt. Etykieta przeciąganego punktu kciuka jest zawsze rysowana na wierzchu innych etykiet, które mogą na nią nachodzić. Jest to ważne, aby użytkownik wiedział, jaką wartość aktualnie wybiera.

Możemy pobrać wybrane wartości, wywołując metodę getValues() z RangeSlider. Zwraca ona tablicę wartości, której rozmiar jest taki sam, jak rozmiar tablicy określonej przez nas w app:values.

Formatowanie etykiet

Bardziej spostrzegawczy mogli zauważyć, że istnieje różnica w formatowaniu etykiet dla wartości podwójnej i potrójnej RangeSliders. Ciągły podwójnie wartościowany RangeSlider używa domyślnego formatu etykiety, podczas gdy dyskretny potrójnie wartościowany ma prefiks Value:. To prowadzi nas ładnie do faktu, że możemy dostosować etykietę:

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 jest interfejsem SAM z pojedynczą getFormattedValue() metodą, która w tym przypadku została zamieniona na lambdę. Użyłem do tego zasobu z ciągiem formatującym:

res/values/strings.xml

1
2
3
4

. <resources>
<string name=”app_name”>Material Slider</string>
<string name=”label_format”>Value: %1$.0f</string>
</resources>

Musimy to zrobić programowo. Jestem prawie pewien, że ciąg formatu taki jak ten pasowałby do większości przypadków użycia, więc byłoby miło, gdybyśmy mogli go określić za pomocą atrybutu XML. Jednak zastosowanie go programowo jest dość trywialne.

Wnioski

Slider i RangeSlider są naprawdę miłymi iteracjami SeekBar. Zapewniają tę samą funkcjonalność plus wiele więcej. Jestem szczególnie pod wrażeniem dbałości o szczegóły – takie jak rysowanie aktywnej etykiety na wierzchu pasywnych. Jest to dobrze zaprojektowana i zaimplementowana kontrola, i w pełni zamierzam jej używać!