school-it.pl // android + kotlin // rozdział 3 // lekcja 12
Spinner — lista rozwijana
Gdy opcji jest dużo — waluty, kraje, klasy w szkole — grupa przycisków opcji zajęłaby cały ekran. Spinner pokazuje tylko wybraną pozycję, a resztę rozwija po dotknięciu. W tej lekcji wypełnisz listę rozwijaną na dwa sposoby: z pliku zasobów i z kodu przez ArrayAdapter, a potem zbudujesz przelicznik walut z dwiema listami.
Czego się dziś nauczysz
- Wypełnisz listę rozwijaną tablicą z
strings.xml. - Wypełnisz ją z kodu za pomocą
ArrayAdapter. - Odczytasz wybraną pozycję i jej numer.
- Zareagujesz na zmianę wyboru obiektem
OnItemSelectedListener. - Ustawisz wybór z kodu metodą
setSelection.
Spinner z zasobów
Najprostszy sposób: lista pozycji w strings.xml jako string-array i atrybut android:entries. Nie trzeba ani linii kodu, żeby lista się wyświetliła:
<string-array name="klasy">
<item>1TP</item>
<item>2TP</item>
<item>3TP</item>
<item>4TP</item>
<item>5TP</item>
</string-array>
<Spinner
android:id="@+id/spKlasa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/klasy" />
po wybraniu pozycji lista sama się zwija
Spinner z kodu: ArrayAdapter
Gdy pozycje powstają w kodzie (np. są wyliczane albo zależą od innego wyboru), lista potrzebuje adaptera. Adapter to pośrednik: bierze tablicę napisów i z każdego napisu robi wiersz listy. Spinner sam nie wie, jak wyświetlić twoje dane — adapter mu to podaje.
val dni = arrayOf("Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek")
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, dni)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spDzien.adapter = adapter
| Fragment | Znaczenie |
|---|---|
ArrayAdapter(this, …, dni) | Adapter dla tablicy dni w tej aktywności. |
android.R.layout.simple_spinner_item | Wbudowany wygląd pozycji widocznej, gdy lista jest zwinięta. |
setDropDownViewResource(…dropdown_item) | Wbudowany wygląd pozycji na rozwiniętej liście — wyższy, wygodny dla palca. |
spDzien.adapter = adapter | Podłączenie adaptera do listy. |
Przedrostek android.R oznacza zasoby wbudowane w system, a nie twoje. Ten sam ArrayAdapter poznasz dokładniej w lekcji 25 — tam wypełnia przewijaną listę ListView.
Odczyt wyboru
| Zapis | Wynik |
|---|---|
sp.selectedItem.toString() | Tekst wybranej pozycji, np. „Środa”. |
sp.selectedItemPosition | Numer wybranej pozycji, od 0 (Środa = 2). |
sp.setSelection(4) | Wybiera pozycję nr 4 z kodu (Piątek). |
sp.count | Liczba pozycji na liście. |
Numer pozycji jest wygodniejszy niż tekst: pasuje do indeksu w tablicy, więc z równoległej tablicy można od razu wziąć związaną z nim wartość — na przykład kurs waluty.
Reakcja na wybór
Spinner nie ma prostego setOnClickListener dla wyboru. Zamiast tego dostaje obiekt słuchacza z dwiema funkcjami: co zrobić po wybraniu pozycji i co zrobić, gdy nic nie jest wybrane. Taki obiekt tworzy się zapisem object : …:
spDzien.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
tvInfo.text = "Wybrano: ${dni[position]}"
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// zwykle pusto — spinner prawie zawsze ma wybraną pozycję
}
}
Nie musisz pisać tych dwóch funkcji z pamięci. Po wpisaniu object : AdapterView.OnItemSelectedListener {} Android Studio podkreśli kod — Alt+Enter → Implement members doda je sam. Z czterech parametrów onItemSelected najważniejszy jest position — numer wybranej pozycji.
onItemSelected wywoła się od razu
Zaraz po starcie ekranu spinner „wybiera” pozycję nr 0 i wywołuje onItemSelected, choć użytkownik niczego nie dotknął. Zwykle to zaleta — wynik od razu jest policzony. Pamiętaj o tym, gdy listener ma pokazywać komunikat w rodzaju „Zmieniłeś wybór”.
Przelicznik walut
Kwota w polu tekstowym i dwie listy rozwijane: z jakiej waluty i na jaką. Wynik przelicza się po każdej zmianie listy i po kliknięciu Przelicz. Przycisk ⇅ zamienia waluty miejscami. Kursy są wpisane w kod jako przykładowe — nie są aktualne.
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Waluty, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt. Listy są wypełniane z kodu, bo nazwa i kurs waluty muszą mieć ten sam numer.
<?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="Przelicznik walut"
android:textSize="24sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etKwota"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Kwota"
android:inputType="numberDecimal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Z waluty" />
<Spinner
android:id="@+id/spZ"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp" />
<Button
android:id="@+id/btnZamien"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="Zamień waluty"
android:text="⇅"
android:textSize="22sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Na walutę" />
<Spinner
android:id="@+id/spNa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp" />
<Button
android:id="@+id/btnPrzelicz"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Przelicz" />
<TextView
android:id="@+id/tvWynik"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvKurs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp" />
</LinearLayout>
package pl.schoolit.waluty
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
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 etKwota = findViewById<EditText>(R.id.etKwota)
val spZ = findViewById<Spinner>(R.id.spZ)
val spNa = findViewById<Spinner>(R.id.spNa)
val btnZamien = findViewById<Button>(R.id.btnZamien)
val btnPrzelicz = findViewById<Button>(R.id.btnPrzelicz)
val tvWynik = findViewById<TextView>(R.id.tvWynik)
val tvKurs = findViewById<TextView>(R.id.tvKurs)
// trzy tablice tej samej długości: pozycja nr 1 w każdej to euro
val nazwy = arrayOf("PLN — złoty polski", "EUR — euro", "USD — dolar amerykański",
"GBP — funt brytyjski", "CZK — korona czeska")
val kody = arrayOf("PLN", "EUR", "USD", "GBP", "CZK")
val kursy = doubleArrayOf(1.0, 4.30, 3.95, 5.10, 0.17) // ile PLN kosztuje 1 jednostka — przykładowe!
// jeden adapter wystarczy dla obu list
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, nazwy)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spZ.adapter = adapter
spNa.adapter = adapter
spNa.setSelection(1) // na start: PLN → EUR
fun przelicz() {
val kwota = etKwota.text.toString().trim().replace(',', '.').toDoubleOrNull()
if (kwota == null || kwota < 0) {
tvWynik.text = "Wpisz kwotę"
tvKurs.text = ""
return
}
val z = spZ.selectedItemPosition
val na = spNa.selectedItemPosition
// przez złotówki: kwota → PLN → waluta docelowa
val wynik = kwota * kursy[z] / kursy[na]
tvWynik.text = "${String.format("%.2f", kwota)} ${kody[z]} = ${String.format("%.2f", wynik)} ${kody[na]}"
tvKurs.text = "kurs przykładowy: 1 ${kody[z]} = ${String.format("%.4f", kursy[z] / kursy[na])} ${kody[na]}"
}
// jeden słuchacz dla obu list
val sluchacz = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
przelicz()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
spZ.onItemSelectedListener = sluchacz
spNa.onItemSelectedListener = sluchacz
btnPrzelicz.setOnClickListener {
przelicz()
}
btnZamien.setOnClickListener {
val z = spZ.selectedItemPosition
spZ.setSelection(spNa.selectedItemPosition)
spNa.setSelection(z)
}
}
}
Co dzieje się po kolei
- Trzy tablice są „równoległe”: element o numerze 3 we wszystkich opisuje funta. Numer wybranej pozycji spinnera jest więc od razu indeksem kursu i kodu.
- Oba spinnery dostają ten sam adapter — pokazują te same waluty.
spNa.setSelection(1)ustawia euro jako walutę docelową. - Kurs to cena jednej jednostki w złotych. Przeliczenie idzie przez złotówki: 250 EUR × 4,30 = 1075 PLN, a 1075 PLN ÷ 1,0 = 1075 PLN.
- Jeden obiekt
sluchaczobsługuje oba spinnery — w obu przypadkach trzeba policzyć to samo. - ⇅ zapamiętuje numer z pierwszej listy w zmiennej, ustawia pierwszą listę na wybór drugiej, a drugą na zapamiętany numer. Oba
setSelectionwywołują słuchacza, więc wynik przelicza się sam. - Zmiana samej kwoty nie przelicza wyniku — do tego jest przycisk Przelicz.
Sprawdź trzy rzeczy
- Uruchom aplikację z pustym polem. Od razu pojawi się „Wpisz kwotę” —
onItemSelectedwywołał się przy starcie. - Ustaw tę samą walutę na obu listach. Kurs wyniesie 1,0000, a kwota się nie zmieni.
- Usuń linię z
setDropDownViewResource. Rozwinięta lista będzie miała niskie, ciasne pozycje, w które trudno trafić palcem.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<Spinner> | kontrolka | Lista rozwijana — jedna wybrana pozycja. |
android:entries="@array/…" | atrybut | Pozycje z string-array. |
ArrayAdapter(this, układ, tablica) | klasa | Zamienia tablicę napisów na pozycje listy. |
android.R.layout.simple_spinner_item | zasób systemu | Wygląd pozycji zwiniętej. |
android.R.layout.simple_spinner_dropdown_item | zasób systemu | Wygląd pozycji rozwiniętej. |
sp.adapter = adapter | właściwość | Podłącza adapter. |
sp.selectedItemPosition | Int | Numer wybranej pozycji od 0. |
sp.selectedItem | obiekt | Wybrana pozycja — z .toString() jako tekst. |
sp.setSelection(nr) | metoda | Wybór z kodu. |
object : AdapterView.OnItemSelectedListener { … } | obiekt | Słuchacz z onItemSelected i onNothingSelected. |
doubleArrayOf(…) | funkcja | Tablica liczb Double. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
sp.setOnClickListener { … } | Awaria: Don’t call setOnClickListener for an AdapterView. Użyj onItemSelectedListener. |
sp.setOnItemClickListener | Też awaria — to metoda dla ListView, nie dla spinnera. |
błąd przy object : AdapterView.OnItemSelectedListener | Object is not abstract and does not implement… — brakuje jednej z dwóch funkcji. Alt+Enter → Implement members. |
R.layout.simple_spinner_item | Unresolved reference — zasób systemowy: android.R.layout…. |
| lista pusta | Brak sp.adapter = adapter albo pusta tablica. |
| tablice nazw i kursów różnej długości | Przy ostatniej pozycji awaria ArrayIndexOutOfBoundsException. Równoległe tablice muszą mieć tyle samo elementów. |
| komunikat „Zmieniono wybór” zaraz po starcie | onItemSelected wywołuje się przy starcie dla pozycji 0. |
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.
Spinner z klasami 1TP–5TP z strings.xml (android:entries) i przycisk Pokaż, który wypisuje „Wybrano klasę 3TP (pozycja 2)”.
pokaż wskazówkę
sp.selectedItem.toString()isp.selectedItemPosition.
Spinner z dniami tygodnia (z kodu). Wybór dnia od razu pokazuje pod spodem plan lekcji na ten dzień (wymyślony, po 5–7 lekcji w osobnych liniach).
pokaż wskazówkę
- Plany trzymaj w tablicy napisów z
\nmiędzy lekcjami; indeks zposition.
Jak przelicznik walut, ale dla długości: mm, cm, m, km, cal, stopa, mila. Wynik przelicza się po zmianie list i po przycisku. Liczby bardzo małe i bardzo duże wypisuj w czytelny sposób (np. 6 cyfr znaczących).
pokaż wskazówkę
- Zamiast kursów do złotówki — ile metrów ma jednostka. Format:
String.format("%.6g", wynik).
Trzy spinnery: rodzaj kawy (espresso 8 zł, latte 12 zł, cappuccino 11 zł, flat white 13 zł), rozmiar (S, M +2 zł, L +4 zł), mleko (krowie, owsiane +2 zł, sojowe +2 zł, bez mleka). Dla espresso spinner mleka ustawia się na „bez mleka” i wyłącza. Cena aktualizuje się po każdej zmianie.
pokaż wskazówkę
- Jeden słuchacz dla trzech spinnerów. Wyłączenie:
spMleko.isEnabled = falseisetSelectionna pozycję „bez mleka”.
Trzy spinnery: dzień (1–31), miesiąc (nazwy) i rok (2000–2030), wypełnione z kodu w pętli. Po każdej zmianie pod spodem: „15 marca 2026” albo — gdy data nie istnieje (31 kwietnia, 29 lutego w roku nieprzestępnym) — „Taka data nie istnieje” na czerwono.
pokaż wskazówkę
- Listę dni zbuduj:
val dni = (1..31).map { it.toString() }. Rok przestępny: podzielny przez 4 i niepodzielny przez 100, albo podzielny przez 400.
Pierwszy spinner: województwo (5 do wyboru). Drugi spinner: miasta z wybranego województwa (po 3–4). Zmiana województwa podmienia zawartość drugiej listy i ustawia ją na pierwszą pozycję. Pod spodem: „Lębork, woj. pomorskie — 35 km do Gdyni” (odległość do Gdyni zapisz w danych). Przycisk Losuj wybiera losowe województwo i losowe miasto.
pokaż wskazówkę
- Miasta: tablica tablic
arrayOf(arrayOf("Gdańsk", "Lębork", …), …). W słuchaczu pierwszej listy przypisz drugiej nowyArrayAdapterz miastami. Przy losowaniu najpierw ustaw województwo, a miasto ustaw dopiero, gdy druga lista ma już nowy adapter (najprościej losować miasto w tym samym kodzie, który podmienia adapter).
Co trzeba zapamiętać
- Stałą listę wpisz do
strings.xmli podaj przezandroid:entries. - Listę z kodu podłączasz przez
ArrayAdapterz układamiandroid.R.layout.simple_spinner_itemi…_dropdown_item. - Wybór:
selectedItemPosition(numer od 0) iselectedItem(tekst); ustawienie:setSelection. - Reakcja:
onItemSelectedListener = object : AdapterView.OnItemSelectedListener { … }— wywołuje się też przy starcie. - Wartości związane z pozycjami trzymaj w równoległych tablicach tej samej długości.
Dokumentacja: Spinner, ArrayAdapter.