Przejdź do treści
School IT / Przestrzeń do nauki

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.

Kotlin XML SeekBar max / progress OnSeekBarChangeListener Color.rgb 1 × 45 min
CEL LEKCJI

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.
TEORIA

Suwak w XML

suwak 0–100, start 50
<SeekBar
    android:id="@+id/sbGlosnosc"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:max="100"
    android:progress="50" />
AtrybutZnaczenie
android:maxNajwiększa wartość (domyślnie 100). Najmniejsza to zawsze 0.
android:progressWartość na starcie.
android:thumbTint, progressTintKolor kółka i wypełnionej części paska.
12:30
progress = 20 (max 100)
progress = 75 (max 100)
thumbTint i progressTint zmienione
trzy suwaki

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.

TEORIA

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.

zakres 10–80
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.

TEORIA

Odczyt i zmiana z kodu

ZapisZnaczenie
sb.progressBieżąca wartość (Int).
sb.progress = 30Przesuwa suwak na 30. Wartość spoza zakresu zostanie przycięta do 0 lub max.
sb.maxOdczyt i zmiana górnej granicy.
sb.isEnabled = falseBlokuje suwak.
TEORIA

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:

słuchacz suwaka
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”
    }
})
FunkcjaKiedy się wywołuje
onProgressChangedPrzy każdej zmianie wartości — także z kodu. Parametr fromUser to true, gdy przesunął użytkownik.
onStartTrackingTouchRaz, w chwili dotknięcia suwaka.
onStopTrackingTouchRaz, w chwili puszczenia suwaka.

Wszystkie trzy funkcje muszą być w obiekcie, nawet jeśli dwie są puste. Alt+Enter → Implement members dopisze je sam.

PRZYKŁAD

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.

12:30
Mikser kolorów
#6750A4
R: 103
G: 80
B: 164
Szary
Losuj
ciemny kolor — biały napis
→przesuwanie
R i G
w prawo
12:31
Mikser kolorów
#FFD54F
R: 255
G: 213
B: 79
Szary
Losuj
jasny kolor — czarny napis

podgląd zmienia się w trakcie przesuwania, nie dopiero po puszczeniu palca

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa MikserKolorow, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt.
res/layout/activity_main.xml — pełny plik
<?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>
MainActivity.kt — pełny plik
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ą przyjmuje setBackgroundColor.
  • %02X zapisuje 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 progress z kodu wywołuje onProgressChanged, więc podgląd odświeża się sam.

Sprawdź trzy rzeczy

  1. Ustaw wszystkie suwaki na 255, potem na 0. Dostaniesz biel #FFFFFF i czerń #000000, a napis zmieni kolor.
  2. Przenieś pokazKolor() z onProgressChanged do onStopTrackingTouch. Kolor zmieni się dopiero po puszczeniu palca — a Losuj przestanie działać, bo zmiana z kodu nie „puszcza palca”.
  3. Kliknij Szary kilka razy — po pierwszym razie nic się już nie zmienia, bo średnia z trzech równych liczb to ta sama liczba.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<SeekBar>kontrolkaSuwak wartości 0–max.
android:max, android:progressatrybutyGórna granica i wartość startowa.
android:thumbTint, progressTintatrybutyKolor kółka i paska.
sb.progressIntOdczyt i ustawienie wartości.
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { … })metodaSłuchacz z trzema funkcjami.
onProgressChanged(…, progress, fromUser)funkcjaKażda zmiana wartości.
onStartTrackingTouch / onStopTrackingTouchfunkcjeDotknięcie / puszczenie suwaka.
Color.rgb(r, g, b)IntKolor ze składowych 0–255.
String.format("%02X", x)StringLiczba szesnastkowo, dwie cyfry.
(0..255).random()IntLosowa liczba z zakresu.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
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.OnSeekBarChangeListenerBrak 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 255max zostało domyślne (100).
Color.rgb(r, g, b) z wartościami > 255Kolor nie będzie taki, jak oczekujesz. Składowe muszą być w zakresie 0–255.
napis wartości pusty do pierwszego ruchuNie wywołano funkcji odświeżającej na starcie — onProgressChanged nie wywołuje się sam przy tworzeniu ekranu.
ZADANIA

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.

ZAD 1Rozmiar tekstu★☆☆

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().
ZAD 2Kalkulator napiwku★☆☆

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.
ZAD 3Przezroczystość i obrót★★☆

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 ustawia progress = 180.
ZAD 4Kalkulator podlewania★★☆

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.
ZAD 5Głośność z wyciszeniem★★☆

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 w onStopTrackingTouch.
ZAD 6Zgadnij kolor★★★

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.
PODSUMOWANIE

Co trzeba zapamiętać

  • SeekBar wybiera liczbę od 0 do max; startową ustawia progress.
  • 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 progress z kodu też wywołuje onProgressChanged.
  • Stan początkowy ekranu ustaw jednym wywołaniem funkcji odświeżającej przed podpięciem słuchacza.

Dokumentacja: SeekBar, Color.

Postęp zapisuje się w tej przeglądarce.