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.
A Slider
jest 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:
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:
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ę:
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:
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ć!