Material Slider

Google släppte Material Components 1.3.0-alpha02 den 16 juli 2020. En komponent som har fått mycket kärlek i den här versionen är den ödmjuka glidaren. Det finns några trevliga tillägg utöver den grundläggande funktionaliteten, och i det här inlägget ska vi utforska dem lite.

Image Source
Licensierat under CC 2.0

En Slider är en användbar kontroll för att låta användaren ange värden utan att behöva använda tangentbordet. Det grundläggande användningsområdet för den är mycket likt Android Framework SeekBar eller AppCompatSeekBar widgets. För dessa finns det olika användningsområden, t.ex. för att skrubba till en viss plats under ljud- eller videouppspelning eller för att ange ett värde inom ett visst intervall. Det finns dock ytterligare några användningsområden som Slider ger oss, vilket vi kommer att se senare i den här artikeln

Vi börjar med att skapa en mycket enkel glidarkontroll och ser hur den ser ut och beter sig.

Grundläggande beteende

Skapa ett materialglidare är mycket enkelt:

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>

Vi anger ett intervall på 0-100 för båda kontrollerna med hjälp av attributen valueFrom och valueTo. Detta ger oss följande beteenden (AppCompatSeekBar är överst och Slider är underst):

Men även om dessa ser väldigt lika ut finns det några viktiga skillnader. För det första är Slider stylad med de färger som definierats i mitt tema medan AppCompatSeekBar använder standardfärger från AppCompat-biblioteket. För det andra är Slider något större, vilket gör den lite tydligare för användaren. Den förmodligen viktigaste skillnaden är tillägget av en etikett till Slider när användaren drar i positionen. Detta gör det lättare för användaren att förstå vilket exakt värde de väljer. Även om vi förvisso kan lägga till en separat vy i vår layout för att visa det här värdet, gör tooltip-etiketten att användargränssnittet blir mindre rörigt. Vi kan inaktivera den om den inte behövs. Genom att lägga till app:labelBehaviour="gone" uppnås detta.

Det finns dock två andra beteenden för etiketter. Det första är standardvärdet som vi kan se i det föregående exemplet – detta är app:labelBehaviour="floating" som låter etiketten flyta över vyn. Det andra alternativet är app:labelBehaviour="withinBounds". Detta renderar på samma sätt som floating men skillnaden är hur det påverkar storleken på Slider. Med floating omfattar kontrollens uppmätta höjd inte etiketten, medan withinBounds omfattar etiketten i den uppmätta höjden. Detta kan vara viktigt beroende på om vi vill försäkra oss om att etiketten inte skymmer andra kontroller i vår layout.

Diskret glidare

Den variant av Slider som vi hittills har tittat på kallas för en kontinuerlig glidare. Det innebär att det värde den returnerar kan vara vilket värde som helst inom intervallet. Det finns dock fall där vi kanske vill begränsa detta. Till exempel returneras bråkvärden av den här kontinuerliga Slider, men vi kan kräva att värdet begränsas till heltal eller ännu större steg. För att uppnå detta kan vi använda ett diskret reglage – som returnerar en diskret uppsättning värden. Vi kan ändra en kontinuerlig Slider till en diskret Slider genom att lägga till ett enda attribut:

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

Införandet av app:stepSize="10" gör att Slider endast returnerar värden som är multipler av 10. Detta ändrar UX något:

För det första finns det kryssmarkeringar som anger de diskreta värden som kommer att returneras. Dessa är ganska subtila, men kan ses tydligt på spåret. Den andra skillnaden är att urvalet inte längre är jämnt utan hoppar mellan de diskreta värdena, vilket vi kan se med hjälp av etikettvärdet. Användaren har en tydlig indikation på att urvalet är från en diskret uppsättning värden.

RangeSlider

Vi kan också använda en annan variant av Slider. Hittills har vi sett hur man väljer ett enskilt värde med hjälp av en Slider, men användaren kan välja ett intervall med hjälp av en RangeSlider.

När vi konfigurerade Slider för första gången använde vi valueFrom och valueTo för att specificera det intervall av värden som skjutreglaget returnerar. en RangeSlider gör det möjligt för användaren att specificera ett intervall. Exempel: Om vi tillhandahåller en sökfunktion i en shoppingapp vill vi kanske att användaren ska kunna ange ett prisintervall. Vi kan uppnå detta med hjälp av a RangeSlider som stöder både kontinuerliga och diskreta lägen:

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

Det finns ett obligatoriskt attribut här: app:values – utan detta faller RangeSlider tillbaka till standardbeteendet Slider. Vi måste ange en array av värden i detta 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″?>
<resurser>
<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>

Dessa arrayer har två respektive tre värden, och den kontinuerliga RangeSlider får intervallet med två värden medan den diskreta RangeSlider får intervallet med tre värden. Detta ger oss några intressanta nya beteenden:

Det översta exemplet visar ett enkelt intervall med två värden som gör det möjligt för användaren att ange ett intervall med övre och undre gränser. Det övergripande intervallet som vi fastställer för RangeSlider begränsar de högsta och lägsta värden som användaren kan ange.

Det nedre exemplet är ännu mer intressant. Förutom att vara ett diskret RangeSlider skapar de tre värdena i arrayen tre tumpunkter i kontrollen. Förutom att bara ha en övre och undre gräns för intervallet kan användaren också ange en punkt någonstans inom den övre och undre gränsen. Användaren kan inte dra den mellersta tummepunkten utanför den övre och nedre gränsen. Etiketten för den tummepunkt som dras ritas alltid ovanpå de andra etiketter som den kan överlappa. Detta är viktigt för att användaren ska veta vilket värde de för närvarande väljer.

Vi kan hämta de valda värdena genom att anropa getValues()-metoden för RangeSlider. Detta returnerar en array av värden vars storlek är densamma som storleken på arrayen som vi specificerade i app:values.

Labelformatering

Den som är mer observant har kanske lagt märke till att det finns en skillnad i etikettformateringen av den dubbla och trippla värderade RangeSliders. Den kontinuerliga dubbelvärderade RangeSlider använder standardformatet för etiketten medan den diskreta trippelvärderade har prefixet Value:. Detta för oss vidare till det faktum att vi kan anpassa etiketten:

MainActivity.kt

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

klass 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 är ett SAM-gränssnitt med en enda getFormattedValue() metod som i det här fallet har omvandlats till en lambda. Jag har använt en formatsträngresurs för detta:

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>

Vi måste göra detta programmatiskt. Jag är ganska säker på att en sådan här formatsträng skulle passa de flesta användningsfall, så det vore bra om vi kunde ange en sådan med hjälp av ett XML-attribut. Att tillämpa en programmatiskt är dock ganska trivialt.

Slutsats

Slider och RangeSlider är riktigt fina iterationer på SeekBar. De ger samma funktionalitet plus mycket mer. Jag är särskilt imponerad av den noggranna detaljutformningen – till exempel att den aktiva etiketten ritas ovanpå den passiva. Detta är en väl utformad och genomförd kontroll, och jag har för avsikt att använda den!