Material Slider

Google heeft op 16 juli 2020 Material Components 1.3.0-alpha02 uitgebracht. Een component die veel liefde heeft gekregen in deze release is de nederige schuifregelaar. Er zijn een aantal leuke toevoegingen bovenop de basisfunctionaliteit, en in deze post zullen we ze een beetje verkennen.

Image Source
Licensed under CC 2.0

Een Slider is een handig besturingselement waarmee de gebruiker waarden kan opgeven zonder het toetsenbord te hoeven gebruiken. Het basisgebruik ervan lijkt sterk op dat van het Android Framework SeekBar of de AppCompatSeekBar-widgets. Voor deze widgets zijn er verschillende toepassingen: zoals het scrollen naar een specifieke locatie tijdens het afspelen van audio of video; of het specificeren van een waarde binnen een bepaald bereik. Er zijn echter een aantal extra gebruiksmogelijkheden die Slider ons biedt, zoals we later in dit artikel zullen zien

We zullen beginnen met het maken van een zeer eenvoudige schuifregelaar en zien hoe het eruit ziet en zich gedraagt.

Basisgedrag

Het maken van een Material Slider is heel eenvoudig:

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>

We specificeren een bereik van 0-100 voor beide besturingselementen met behulp van de valueFrom en valueTo attributen. Dit geeft ons de volgende gedragingen (AppCompatSeekBar is bovenaan, en Slider is onderaan):

Hoewel deze erg op elkaar lijken, zijn er toch enkele belangrijke verschillen. Ten eerste is Slider opgemaakt met de kleuren die in mijn thema zijn gedefinieerd, terwijl AppCompatSeekBar standaardkleuren uit de AppCompat-bibliotheek gebruikt. Ten tweede, de Slider is iets groter waardoor het een beetje duidelijker is voor de gebruiker. Waarschijnlijk het belangrijkste verschil is de toevoeging van een label aan de Slider wanneer de gebruiker de positie versleept. Dit maakt het gemakkelijker voor de gebruiker om te begrijpen welke waarde hij precies selecteert. Hoewel we zeker een aparte View aan onze layout kunnen toevoegen om deze waarde weer te geven, maakt het tooltip label de UI minder onoverzichtelijk. We kunnen dat uitschakelen als het niet nodig is. Het toevoegen van app:labelBehaviour="gone" zal dit bereiken.

Echter zijn er twee andere label gedragingen. De eerste is de standaard die we kunnen zien in het vorige voorbeeld – dit is app:labelBehaviour="floating" die het label boven de view laat zweven. De andere optie is app:labelBehaviour="withinBounds". Dit rendert hetzelfde als floating, maar het verschil is hoe het de grootte van Slider beïnvloedt. Met floating wordt het label niet meegenomen in de gemeten hoogte van het besturingselement, terwijl withinBounds het label wel meeneemt in de gemeten hoogte. Dit kan belangrijk zijn, afhankelijk van de vraag of we ervoor willen zorgen dat het label geen andere besturingselementen in onze lay-out aan het oog onttrekt.

Discrete Slider

De variant van Slider die we tot nu toe hebben bekeken, staat bekend als een continue schuifregelaar. Dat betekent dat de waarde die het teruggeeft elke waarde binnen het bereik kan zijn. Er zijn echter gevallen waarin we dit willen beperken. Bijvoorbeeld, fractionele waarden worden geretourneerd door deze continue Slider, maar wij kunnen verlangen dat de waarde wordt beperkt tot gehele getallen of zelfs grotere stappen. Om dit te bereiken kunnen we een discrete schuifregelaar gebruiken – die een discrete reeks waarden teruggeeft. We kunnen een continue Slider veranderen in een discrete door een enkel attribuut toe te voegen:

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

Door app:stepSize="10" toe te voegen, retourneert Slider alleen waarden die veelvouden van 10 zijn. Dit verandert de UX enigszins:

Eerst zijn er vinkjes die de discrete waarden aangeven die zullen worden teruggegeven. Deze zijn heel subtiel, maar duidelijk te zien op het spoor. Het tweede verschil is dat de selectie niet langer vloeiend verloopt, maar springt tussen de discrete waarden, zoals we kunnen zien aan de labelwaarde. De gebruiker heeft een duidelijke aanwijzing dat het gaat om een selectie uit een discrete reeks waarden.

RangeSlider

We kunnen ook een andere variant van Slider gebruiken. Tot nu toe hebben we gezien hoe een enkele waarde kan worden geselecteerd met behulp van een Slider, maar de gebruiker kan een bereik selecteren met behulp van een RangeSlider.

Toen we Slider voor het eerst configureerden, gebruikten we valueFrom en valueTo om dat bereik van waarden op te geven dat de schuifbalk teruggeeft. een RangeSlider stelt de gebruiker in staat een bereik op te geven. Bijvoorbeeld: als wij een zoekfaciliteit binnen een het winkelen app verstrekken, zouden wij kunnen wensen dat de gebruiker een prijswaaier kan specificeren. We kunnen dit bereiken met een RangeSlider die zowel continue als discrete modes ondersteunt:

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

Er is hier een verplicht attribuut: app:values – zonder dit valt RangeSlider terug op standaard Slider gedrag. We moeten in dit attribuut een array van waarden opgeven:

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>

Deze arrays hebben respectievelijk twee en drie waarden, en de continue RangeSlider krijgt het bereik met twee waarden, terwijl de discrete RangeSlider het bereik met drie waarden krijgt. Dit levert ons enkele interessante nieuwe gedragingen op:

Het bovenste voorbeeld toont een eenvoudig bereik met twee waarden dat de gebruiker in staat stelt een bereik met boven- en ondergrenzen op te geven. Het algemene bereik dat we voor RangeSlider hebben ingesteld, beperkt de maximale en minimale waarden die de gebruiker kan opgeven.

Het onderste voorbeeld is nog interessanter. Behalve dat het een discrete RangeSlider is, creëren de drie waarden in de array drie duimpunten in de controle. De gebruiker kan niet alleen een boven- en ondergrens voor het bereik opgeven, maar ook een punt ergens binnen de boven- en ondergrens. De gebruiker kan het middelste duimpunt niet buiten de door de bovenste en onderste punten bepaalde grenzen slepen. Het label van het gesleepte duimpunt wordt altijd getekend bovenop de andere labels die het zou kunnen overlappen. Dit is belangrijk zodat de gebruiker weet welke waarde hij op dat moment selecteert.

We kunnen de geselecteerde waarden opvragen door de methode getValues() van RangeSlider op te roepen. Dit levert een array van waarden op waarvan de grootte gelijk is aan de grootte van de array die we in app:values hebben opgegeven.

Labelopmaak

Het is de oplettende gebruiker misschien opgevallen dat er een verschil is in de labelopmaak van de dubbele en drievoudig gewaardeerde RangeSliders. De continue dubbele waarde RangeSlider gebruikt de standaard label-indeling, terwijl de discrete drievoudige waarde het voorvoegsel Value: heeft. Dit brengt ons mooi op het feit dat we het label kunnen aanpassen:

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 is een SAM-interface met een enkele getFormattedValue() methode die in dit geval is geconverteerd naar een lambda. Ik heb hiervoor een format string resource gebruikt:

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>

We moeten dit programmatisch doen. Ik ben er vrij zeker van dat een format string als deze in de meeste gevallen zou passen, dus het zou mooi zijn als we er een zouden kunnen specificeren met behulp van een XML attribuut. Het is echter vrij triviaal om er een programmatisch toe te passen.

Conclusie

Slider en RangeSlider zijn echt leuke iteraties op SeekBar. Ze bieden dezelfde functionaliteit plus nog veel meer. Ik ben vooral onder de indruk van de aandacht voor details – zoals het tekenen van het actieve label boven op het passieve. Dit is een goed ontworpen en geïmplementeerde controle, en ik ben volledig van plan om het te gebruiken!