Material Slider

Google frigav Material Components 1.3.0-alpha02 den 16. juli 2020. En komponent, der har fået en masse kærlighed i denne udgivelse, er den ydmyge slider. Der er nogle gode tilføjelser oven på den grundlæggende funktionalitet, og i dette indlæg vil vi udforske dem lidt.

Image Source
Licensed under CC 2.0

En Slider er en nyttig kontrol, der gør det muligt for brugeren at angive værdier uden at skulle bruge tastaturet. Den grundlæggende brugssituation for den er meget lig Android Framework SeekBar eller AppCompatSeekBar-widgets. For disse er der forskellige anvendelsesmuligheder: f.eks. scrubbing til et bestemt sted under lyd- eller videoafspilning eller angivelse af en værdi inden for et givet interval. Der er dog nogle yderligere anvendelsesmuligheder, som Slider giver os, som vi vil se senere i denne artikel

Vi begynder med at oprette en meget simpel skyderkontrol og ser, hvordan den ser ud og opfører sig.

Basisadfærd

Det er meget ligetil at oprette en materialeskieberegulering:

res/layout/activity_main.xml

1
2
3
4
5
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>

Vi angiver et interval på 0-100 på begge kontrolelementer ved hjælp af attributterne valueFrom og valueTo. Dette giver os følgende adfærd (AppCompatSeekBar er øverst, og Slider er nederst):

Mens disse ser meget ens ud, er der nogle vigtige forskelle. For det første er Slider stylet ved hjælp af de farver, der er defineret i mit tema, mens AppCompatSeekBar bruger standardfarver fra AppCompat-biblioteket. For det andet er Slider lidt større, hvilket gør den lidt tydeligere for brugeren. Den vigtigste forskel er nok tilføjelsen af en etiket til Slider, når brugeren trækker i positionen. Dette gør det lettere for brugeren at forstå, hvilken præcis værdi han/hun vælger. Selv om vi bestemt kan tilføje en separat visning til vores layout for at vise denne værdi, gør tooltip-etiketten UI’en mindre rodet. Vi kan deaktivere det, hvis det ikke er nødvendigt. Tilføjelse af app:labelBehaviour="gone" vil opnå dette.

Der er imidlertid to andre etiketadfærdsmåder. Den første er den standard, som vi kan se i det foregående eksempel – det er app:labelBehaviour="floating", som lader etiketten svæve over visningen. Den anden mulighed er app:labelBehaviour="withinBounds". Dette renderer det samme som floating, men forskellen er, hvordan det påvirker størrelsen af Slider. Med floating omfatter den målte højde af kontrolelementet ikke etiketten, mens withinBounds omfatter etiketten i den målte højde. Dette kan være vigtigt afhængigt af, om vi ønsker at sikre, at etiketten ikke skjuler andre kontroller i vores layout.

Diskret skyder

Den variant af Slider, som vi hidtil har set på, er kendt som en kontinuerlig skyder. Det betyder, at den værdi, den returnerer, kan være en hvilken som helst værdi inden for området. Der er dog tilfælde, hvor vi måske ønsker at begrænse dette. For eksempel returneres brøkværdier af denne kontinuerlige Slider, men vi kan kræve, at værdien skal begrænses til hele tal eller endnu større trin. For at opnå dette kan vi bruge en diskret skyder – som returnerer et diskret sæt af værdier. Vi kan ændre en kontinuerlig Slider til en diskret Slider ved at tilføje en enkelt egenskab:

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

Tilføjelse af app:stepSize="10" gør, at Slider kun returnerer værdier, der er multipla af 10. Dette ændrer UX en smule:

For det første er der krydsmarkeringer, som angiver de diskrete værdier, der returneres. Disse er ret subtile, men kan ses tydeligt på banen. Den anden forskel er, at markeringen ikke længere er jævn, men hopper mellem de diskrete værdier, som vi kan se på labelværdien. Brugeren har en klar indikation af, at valget er fra et diskret sæt af værdier.

RangeSlider

Vi kan også bruge en anden variant af Slider. Hidtil har vi set, hvordan man vælger en enkelt værdi ved hjælp af en Slider, men brugeren kan vælge et interval ved hjælp af en RangeSlider.

Da vi først konfigurerede Slider, brugte vi valueFrom og valueTo til at angive det interval af værdier, som skyderen returnerer. en RangeSlider giver brugeren mulighed for at angive et interval. Eksempel: Hvis vi f.eks. leverede en søgefunktion i en shopping-app, ville vi måske ønske, at brugeren kunne angive et prisinterval. Det kan vi opnå ved hjælp af a RangeSlider, som understøtter både kontinuerlige og diskrete tilstande:

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

Der er en obligatorisk attribut her: app:values – uden denne falder RangeSlider tilbage til standard Slider-adfærd. Vi skal angive et array af værdier i denne attribut:

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>

Disse arrays har henholdsvis to og tre værdier, og den kontinuerlige RangeSlider får området med to værdier, mens den diskrete RangeSlider får området med tre værdier. Dette giver os nogle interessante nye adfærdsmønstre:

Det øverste eksempel viser et simpelt interval med to værdier, som giver brugeren mulighed for at angive et interval med øvre og nedre grænser. Det overordnede område, som vi indstiller for RangeSlider, begrænser de maksimum- og minimumsværdier, som brugeren kan angive.

Det nederste eksempel er endnu mere interessant. Ud over at være et diskret RangeSlider skaber de tre værdier i arrayet tre tommelfingerpunkter i kontrolelementet. Ud over blot en øvre og en nedre grænse for intervallet kan brugeren også angive et punkt et sted inden for den øvre og den nedre grænse. Brugeren kan ikke trække det midterste tommelfingerpunkt uden for de grænser, der er fastsat af det øvre og det nedre punkt. Etiketten for det tommelfingerpunkt, der trækkes, tegnes altid oven på de andre etiketter, som den eventuelt overlapper. Dette er vigtigt, så brugeren ved, hvilken værdi han/hun er ved at vælge i øjeblikket.

Vi kan hente de valgte værdier ved at kalde getValues()-metoden i RangeSlider. Dette returnerer et array af værdier, hvis størrelse er den samme som størrelsen af det array, vi har angivet i app:values.

Labelformatering

Den mere opmærksomme har måske bemærket, at der er en forskel i labelformateringen af den dobbelt- og trippelvurderede RangeSliders. Den kontinuerlige dobbeltværdi RangeSlider anvender standard-etiketformatet, mens den diskrete tredobbeltværdi har præfikset Value:. Dette bringer os pænt videre til, at vi kan tilpasse etiketten:

MainActivity.kt

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

klasse 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 er en SAM-grænseflade med en enkelt getFormattedValue() metode, som i dette tilfælde er blevet konverteret til en lambda. Jeg har brugt en formatstrengressource til dette:

res/values/strings.xml

1
2
3
4

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

Vi skal gøre dette programmæssigt. Jeg er ret sikker på, at en formatstreng som denne ville passe til de fleste anvendelsestilfælde, så det ville være rart, hvis vi kunne angive en sådan ved hjælp af en XML-attribut. Det er dog ret trivielt at anvende en programmatisk.

Conclusion

Slider og RangeSlider er rigtig fine iterationer på SeekBar. De giver den samme funktionalitet plus meget mere. Jeg er især imponeret over opmærksomheden på detaljerne – som f.eks. at tegne den aktive etiket oven på de passive etiketter. Dette er en veludformet og implementeret kontrol, og jeg har fuldt ud til hensigt at bruge den!