school-it.pl // android + kotlin // rozdział 4 // lekcja 14
SeekBar — suwak
SeekBar to suwak: użytkownik przesuwa palcem kółko po pasku i wybiera liczbę z zakresu. Głośność, jasność ekranu, rozmiar pędzla, wysokość napiwku — wszędzie tam, gdzie dokładna wartość jest mniej ważna niż szybkie „trochę więcej, trochę mniej”. W tej lekcji zbudujesz mikser kolorów: trzy suwaki ustawiają ilość czerwieni, zieleni i błękitu, a podgląd koloru zmienia się w trakcie przesuwania.
Czego się dziś nauczysz
- Dodasz suwak z ustalonym zakresem i wartością startową.
- Uzyskasz zakres zaczynający się od innej liczby niż 0.
- Odczytasz i ustawisz wartość suwaka z kodu.
- Zareagujesz na przesuwanie obiektem
OnSeekBarChangeListener. - Złożysz kolor z trzech składowych funkcją
Color.rgb.
Suwak w XML
<SeekBar
android:id="@+id/sbGlosnosc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progress="50" />
| Atrybut | Znaczenie |
|---|---|
android:max | Największa wartość (domyślnie 100). Najmniejsza to zawsze 0. |
android:progress | Wartość na starcie. |
android:thumbTint, progressTint | Kolor kółka i wypełnionej części paska. |
Slider z biblioteki Material
Biblioteka Material ma nowocześniejszy suwak com.google.android.material.slider.Slider — z liczbami ułamkowymi, krokiem i dymkiem z wartością. Działa podobnie, ale ma inne nazwy metod. W kursie używamy klasycznego SeekBar, który występuje w zadaniach egzaminacyjnych.
Zakres od innej liczby niż 0
Suwak zawsze zaczyna od 0. Atrybut android:min istnieje, ale działa dopiero od Androida 8 (API 26), a nasz projekt obsługuje API 24. Zakres przesuwamy więc w kodzie: na przykład dla wieku 10–80 ustawiamy max="70" i do odczytu dodajemy 10.
val MIN = 10
// w XML: android:max="70" (80 − 10)
val wiek = sbWiek.progress + MIN // 0 → 10, 70 → 80
Krok inny niż 1 robi się tak samo: suwak 0–20 i mnożenie przez 5 daje wartości 0, 5, 10, … 100.
Odczyt i zmiana z kodu
| Zapis | Znaczenie |
|---|---|
sb.progress | Bieżąca wartość (Int). |
sb.progress = 30 | Przesuwa suwak na 30. Wartość spoza zakresu zostanie przycięta do 0 lub max. |
sb.max | Odczyt i zmiana górnej granicy. |
sb.isEnabled = false | Blokuje suwak. |
Reakcja na przesuwanie
Jak przy spinnerze (lekcja 12), słuchacz suwaka to obiekt z kilkoma funkcjami — tu trzema. Android wywołuje je w różnych chwilach przesuwania:
sbGlosnosc.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
tvGlosnosc.text = "Głośność: $progress" // przy każdej zmianie, wiele razy na sekundę
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
// palec dotknął suwaka
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
// palec puścił suwak — dobre miejsce na „zapisz ustawienie”
}
})
| Funkcja | Kiedy się wywołuje |
|---|---|
onProgressChanged | Przy każdej zmianie wartości — także z kodu. Parametr fromUser to true, gdy przesunął użytkownik. |
onStartTrackingTouch | Raz, w chwili dotknięcia suwaka. |
onStopTrackingTouch | Raz, w chwili puszczenia suwaka. |
Wszystkie trzy funkcje muszą być w obiekcie, nawet jeśli dwie są puste. Alt+Enter → Implement members dopisze je sam.
Mikser kolorów RGB
Trzy suwaki 0–255 ustawiają składowe koloru: czerwoną (R), zieloną (G) i niebieską (B). Duży prostokąt pokazuje wynik, a pod nim widać zapis koloru #RRGGBB, gotowy do wklejenia w colors.xml. Napis w podglądzie robi się biały na ciemnym kolorze i czarny na jasnym. Przyciski Losuj i Szary ustawiają suwaki z kodu.
R i G
w prawo
podgląd zmienia się w trakcie przesuwania, nie dopiero po puszczeniu palca
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
MikserKolorow, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="16dp"
android:fitsSystemWindows="true">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Mikser kolorów"
android:textSize="24sp"
android:textStyle="bold" />
<!-- podgląd koloru z napisem kodu -->
<TextView
android:id="@+id/tvPodglad"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_marginTop="12dp"
android:fontFamily="monospace"
android:gravity="center"
android:textSize="28sp" />
<TextView
android:id="@+id/tvR"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
<SeekBar
android:id="@+id/sbR"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="255"
android:progress="103"
android:progressTint="#E53935"
android:thumbTint="#E53935" />
<TextView
android:id="@+id/tvG"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<SeekBar
android:id="@+id/sbG"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="255"
android:progress="80"
android:progressTint="#43A047"
android:thumbTint="#43A047" />
<TextView
android:id="@+id/tvB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<SeekBar
android:id="@+id/sbB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="255"
android:progress="164"
android:progressTint="#1E88E5"
android:thumbTint="#1E88E5" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
android:id="@+id/btnSzary"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Szary" />
<Button
android:id="@+id/btnLosuj"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Losuj" />
</LinearLayout>
</LinearLayout>
package pl.schoolit.mikserkolorow
import android.graphics.Color
import android.os.Bundle
import android.widget.Button
import android.widget.SeekBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tvPodglad = findViewById<TextView>(R.id.tvPodglad)
val tvR = findViewById<TextView>(R.id.tvR)
val tvG = findViewById<TextView>(R.id.tvG)
val tvB = findViewById<TextView>(R.id.tvB)
val sbR = findViewById<SeekBar>(R.id.sbR)
val sbG = findViewById<SeekBar>(R.id.sbG)
val sbB = findViewById<SeekBar>(R.id.sbB)
val btnSzary = findViewById<Button>(R.id.btnSzary)
val btnLosuj = findViewById<Button>(R.id.btnLosuj)
fun pokazKolor() {
val r = sbR.progress
val g = sbG.progress
val b = sbB.progress
tvR.text = "R (czerwony): $r"
tvG.text = "G (zielony): $g"
tvB.text = "B (niebieski): $b"
val kolor = Color.rgb(r, g, b) // kolor z trzech składowych 0–255
tvPodglad.setBackgroundColor(kolor)
tvPodglad.text = String.format("#%02X%02X%02X", r, g, b)
// jasność odczuwana przez oko: zielony liczy się najbardziej
val jasnosc = (r * 299 + g * 587 + b * 114) / 1000
if (jasnosc > 150) {
tvPodglad.setTextColor(Color.BLACK)
} else {
tvPodglad.setTextColor(Color.WHITE)
}
}
pokazKolor()
// jeden słuchacz dla trzech suwaków
val sluchacz = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
pokazKolor()
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
}
sbR.setOnSeekBarChangeListener(sluchacz)
sbG.setOnSeekBarChangeListener(sluchacz)
sbB.setOnSeekBarChangeListener(sluchacz)
btnLosuj.setOnClickListener {
sbR.progress = (0..255).random()
sbG.progress = (0..255).random()
sbB.progress = (0..255).random()
// pokazKolor() wywoła słuchacz — zmiana z kodu też go uruchamia
}
btnSzary.setOnClickListener {
// szary: wszystkie składowe równe — bierzemy ich średnią
val srednia = (sbR.progress + sbG.progress + sbB.progress) / 3
sbR.progress = srednia
sbG.progress = srednia
sbB.progress = srednia
}
}
}
Co dzieje się po kolei
- Każdy suwak ma
max="255"— tyle wynosi największa wartość składowej koloru. Wartości startowe dają fiolet motywu. pokazKolor()czyta wszystkie trzy suwaki naraz, więc nie ma znaczenia, który został przesunięty.Color.rgb(r, g, b)skleja składowe w jedną liczbę koloru, którą przyjmujesetBackgroundColor.%02Xzapisuje liczbę szesnastkowo, wielkimi literami, na dwóch znakach: 103 →67, 5 →05.- Wzór na jasność waży składowe tak, jak widzi je oko — czysty zielony wydaje się jaśniejszy niż czysty niebieski. Od tej jasności zależy kolor napisu.
- Losuj i Szary tylko przestawiają suwaki. Zmiana
progressz kodu wywołujeonProgressChanged, więc podgląd odświeża się sam.
Sprawdź trzy rzeczy
- Ustaw wszystkie suwaki na 255, potem na 0. Dostaniesz biel
#FFFFFFi czerń#000000, a napis zmieni kolor. - Przenieś
pokazKolor()zonProgressChangeddoonStopTrackingTouch. Kolor zmieni się dopiero po puszczeniu palca — a Losuj przestanie działać, bo zmiana z kodu nie „puszcza palca”. - Kliknij Szary kilka razy — po pierwszym razie nic się już nie zmienia, bo średnia z trzech równych liczb to ta sama liczba.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<SeekBar> | kontrolka | Suwak wartości 0–max. |
android:max, android:progress | atrybuty | Górna granica i wartość startowa. |
android:thumbTint, progressTint | atrybuty | Kolor kółka i paska. |
sb.progress | Int | Odczyt i ustawienie wartości. |
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { … }) | metoda | Słuchacz z trzema funkcjami. |
onProgressChanged(…, progress, fromUser) | funkcja | Każda zmiana wartości. |
onStartTrackingTouch / onStopTrackingTouch | funkcje | Dotknięcie / puszczenie suwaka. |
Color.rgb(r, g, b) | Int | Kolor ze składowych 0–255. |
String.format("%02X", x) | String | Liczba szesnastkowo, dwie cyfry. |
(0..255).random() | Int | Losowa liczba z zakresu. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
android:min="10" | Ostrzeżenie Attribute min is only used in API level 26 and higher — na starszych telefonach suwak i tak zacznie od 0. Przesuwaj zakres w kodzie. |
błąd przy object : SeekBar.OnSeekBarChangeListener | Brak którejś z trzech funkcji — Alt+Enter → Implement members. |
sb.setOnClickListener { … } | Reaguje tylko na krótkie dotknięcie, a nie na przesuwanie. |
| wartość nigdy nie dochodzi do 255 | max zostało domyślne (100). |
Color.rgb(r, g, b) z wartościami > 255 | Kolor nie będzie taki, jak oczekujesz. Składowe muszą być w zakresie 0–255. |
| napis wartości pusty do pierwszego ruchu | Nie wywołano funkcji odświeżającej na starcie — onProgressChanged nie wywołuje się sam przy tworzeniu ekranu. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Cały kod pisz w onCreate. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Napis „Android” i suwak, który zmienia jego rozmiar w zakresie 12–72 sp. Pod suwakiem: „Rozmiar: 36 sp”.
pokaż wskazówkę
max="60", rozmiar =progress + 12, ustawienie:tv.textSize = rozmiar.toFloat().
Pole z kwotą rachunku i suwak napiwku 0–25% z krokiem 5%. Napiwek i suma przeliczają się w trakcie przesuwania.
pokaż wskazówkę
- Krok 5:
max="5", procent =progress * 5.
Obrazek (ikona z Vector Asset) i dwa suwaki: przezroczystość 0–100% i obrót od −180° do 180°. Przycisk Reset ustawia 100% i 0°.
pokaż wskazówkę
alpha = progress / 100f. Obrót:max="360", kąt =progress - 180; Reset ustawiaprogress = 180.
Kalkulator ilości wody dla rośliny doniczkowej: suwaki „średnica doniczki” (8–40 cm), „temperatura w pokoju” (15–30 °C), „nasłonecznienie” (0–10). Wynik w mililitrach: średnica × 10 ml, plus 3% za każdy stopień powyżej 20 °C, plus 5% za każdy punkt nasłonecznienia. Wynik przelicza się w trakcie przesuwania każdego suwaka.
pokaż wskazówkę
- Jeden słuchacz dla trzech suwaków i jedna funkcja
przelicz()— jak w przykładzie. Każdy suwak ma własne przesunięcie zakresu.
Suwak głośności 0–100, ikona głośnika (Vector Asset: volume up, volume down, volume off — zależnie od wartości: 0, 1–50, 51–100) i przycisk Wycisz, który ustawia 0, a przy ponownym kliknięciu przywraca poprzednią wartość. Napis „Zapisano: 70” pojawia się dopiero po puszczeniu suwaka.
pokaż wskazówkę
- Ikonę zmieniaj w
onProgressChanged, napis „Zapisano” ustawiaj wonStopTrackingTouch.
Gra: na górze prostokąt w losowym kolorze (cel), pod nim prostokąt sterowany trzema suwakami RGB. Przycisk Sprawdź liczy podobieństwo w procentach: 100% − (suma różnic składowych / 765 × 100%). Wynik ≥ 90% to „Świetnie!”, a po kliknięciu Nowy kolor losuje się następny cel. Zapisuj najlepszy wynik w rundzie.
pokaż wskazówkę
- Różnica składowej:
kotlin.math.abs(sbR.progress - celR). Liczba 765 to 3 × 255 — największa możliwa suma różnic.
Co trzeba zapamiętać
SeekBarwybiera liczbę od 0 domax; startową ustawiaprogress.- Zakres od innej liczby niż 0 i krok inny niż 1 robisz w kodzie: dodawanie i mnożenie.
- Słuchacz to obiekt z trzema funkcjami; najważniejsza jest
onProgressChanged. - Zmiana
progressz kodu też wywołujeonProgressChanged. - Stan początkowy ekranu ustaw jednym wywołaniem funkcji odświeżającej przed podpięciem słuchacza.