school-it.pl // android + kotlin // rozdział 5 // lekcja 18
Selektory stanów i style
Przycisk z kształtem z lekcji 17 wygląda tak samo, gdy go naciskasz, i tak samo, gdy jest wyłączony — użytkownik nie widzi reakcji. Selektor dobiera wygląd do stanu kontrolki: inny przy naciśnięciu, inny po wyłączeniu, inny po zaznaczeniu. Styl zbiera powtarzające się atrybuty w jednym miejscu, więc dwanaście jednakowych przycisków nie wymaga dwunastu kopii tych samych linii. Oba połączysz, budując klawiaturę PIN.
Czego się dziś nauczysz
- Zbudujesz selektor tła z osobnym kształtem dla stanu naciśnięcia i wyłączenia.
- Zbudujesz selektor koloru tekstu w folderze
res/color. - Sterujesz wyglądem z kodu przez stan
isSelected. - Zapiszesz wspólne atrybuty w stylu i użyjesz go w wielu kontrolkach.
- Zmienisz kolor główny całej aplikacji w motywie.
Selektor tła
Selektor to plik w res/drawable z elementem <selector>. Zawiera listę <item> — każdy opisuje wygląd w innym stanie. Android sprawdza je od góry i bierze pierwszy pasujący:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 1. wyłączony -->
<item android:state_enabled="false">
<shape android:shape="rectangle">
<solid android:color="#E0E0E0" />
<corners android:radius="12dp" />
</shape>
</item>
<!-- 2. naciśnięty -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#4F378B" />
<corners android:radius="12dp" />
</shape>
</item>
<!-- 3. każdy inny stan — ZAWSZE na końcu, bez warunku -->
<item>
<shape android:shape="rectangle">
<solid android:color="#6750A4" />
<corners android:radius="12dp" />
</shape>
</item>
</selector>
Kształt można wpisać bezpośrednio w <item> (jak wyżej) albo wskazać osobny plik: <item android:state_pressed="true" android:drawable="@drawable/ksztalt_nacisniety" />. Selektor podpinasz jak zwykły kształt: android:background="@drawable/tlo_przycisk" — na AppCompatButton (lekcja 17).
Kolejność ma znaczenie
Element bez warunku pasuje do każdego stanu. Gdy stoi na początku, Android zawsze wybierze jego i nigdy nie dojdzie do stanów niżej. Element domyślny zawsze dawaj na koniec.
Stany kontrolki
| Stan | Kiedy jest prawdziwy | Kto go ustawia |
|---|---|---|
state_pressed | Palec dotyka kontrolki. | Android, sam. |
state_enabled | Kontrolka jest włączona. | Ty: isEnabled. |
state_checked | Pole wyboru / przełącznik zaznaczony. | Użytkownik lub isChecked. |
state_selected | Kontrolka oznaczona jako wybrana. | Tylko ty: isSelected = true. |
state_focused | Kontrolka ma fokus (np. pole tekstowe z kursorem). | Android. |
Stan state_selected jest szczególnie wygodny: każda kontrolka, nawet zwykły View, ma właściwość isSelected, a selektor sam zmieni jej wygląd. Nie trzeba w kodzie podmieniać tła ani kolorów — wystarczy przestawić true / false.
Selektor koloru tekstu
Gdy tło naciśniętego przycisku robi się ciemne, tekst też powinien zmienić kolor. Selektor koloru umieszcza się w osobnym folderze res/color (prawy przycisk na res → New → Android Resource Directory → typ color), a jego elementy mają atrybut android:color:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#9E9E9E" android:state_enabled="false" />
<item android:color="#FFFFFF" android:state_pressed="true" />
<item android:color="#1D1B20" />
</selector>
Używasz go jak zwykłego koloru: android:textColor="@color/tekst_przycisk".
Styl — wspólne atrybuty
Dwanaście przycisków klawiatury ma ten sam rozmiar, tło, kolor i wielkość tekstu. Zamiast powtarzać te atrybuty dwanaście razy, zapisujesz je raz jako styl w res/values/themes.xml (albo w osobnym pliku styles.xml w tym samym folderze):
<style name="Klawisz">
<item name="android:layout_width">72dp</item>
<item name="android:layout_height">72dp</item>
<item name="android:textSize">26sp</item>
<item name="android:background">@drawable/tlo_klawisz</item>
</style>
<!-- styl potomny: wszystko z Klawisz + czerwony tekst -->
<style name="Klawisz.Usun">
<item name="android:textColor">#C62828</item>
</style>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn7"
style="@style/Klawisz"
android:text="7" />
| Zasada | Znaczenie |
|---|---|
| atrybut w kontrolce wygrywa ze stylem | Styl ustala wartości domyślne; kontrolka może nadpisać dowolną z nich. |
Klawisz.Usun | Nazwa z kropką tworzy styl potomny — dziedziczy wszystko po Klawisz. |
parent="…" | Inny zapis dziedziczenia, np. po stylu biblioteki Material: parent="Widget.Material3.Button". |
| jeden styl na kontrolkę | Atrybut style przyjmuje tylko jeden styl. |
Motyw aplikacji
W tym samym pliku themes.xml jest już styl o nazwie Theme.NazwaProjektu. To motyw — styl dla całej aplikacji, podpięty w manifeście. Zmienia wygląd wszystkich ekranów naraz. Najważniejszy jest kolor główny:
<style name="Base.Theme.Pin" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">#00696D</item> <!-- przyciski, przełączniki, suwaki -->
<item name="colorOnPrimary">#FFFFFF</item> <!-- tekst na kolorze głównym -->
</style>
Po tej zmianie wszystkie zwykłe przyciski, przełączniki, pola wyboru i suwaki w aplikacji staną się morskie — bez dotykania układów. Kontrolki z własnym tłem (selektorem) zachowają swój wygląd. Uwaga: plik themes.xml jest też w folderze values-night — dla trybu ciemnego. Zmieniaj kolory w obu.
Klawiatura PIN
Ekran blokady z okrągłymi klawiszami. Wszystkie klawisze korzystają z jednego stylu, a selektor pokazuje naciśnięcie i blokadę. Cztery kropki nad klawiaturą wypełniają się w miarę wpisywania — selektorem sterowanym przez isSelected. Poprawny PIN (2580) odblokowuje ekran. Po trzech błędnych próbach klawiatura blokuje się na 10 sekund — klawisze szarzeją same dzięki stanowi state_enabled.
wygląd wyłączonych klawiszy zmienia selektor — kod ustawia tylko isEnabled = false
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Pin, Kotlin, API 24. - Utwórz folder
res/colori w nim pliktekst_klawisz.xml. Wres/drawableutwórztlo_klawisz.xmlikropka.xml(Root element:selector). - Dopisz dwa style do
res/values/themes.xml— pod istniejącymi stylami motywu, wewnątrz<resources>. - Podmień
activity_main.xmliMainActivity.kt.
├── color/
│ └── tekst_klawisz.xmlnowy folder i plik
├── drawable/
│ ├── tlo_klawisz.xmlselektor tła klawisza
│ └── kropka.xmlselektor kropki
├── layout/
│ └── activity_main.xml
└── values/
└── themes.xmldopisane style Klawisz i Klawisz.Usun
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false">
<shape android:shape="oval">
<solid android:color="#E0E0E0" />
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="#6750A4" />
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="#E8DEF8" />
</shape>
</item>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#9E9E9E" android:state_enabled="false" />
<item android:color="#FFFFFF" android:state_pressed="true" />
<item android:color="#1D1B20" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- wybrana: pełne koło -->
<item android:state_selected="true">
<shape android:shape="oval">
<solid android:color="#6750A4" />
</shape>
</item>
<!-- pozostałe: sam obrys -->
<item>
<shape android:shape="oval">
<stroke android:width="2dp" android:color="#79747E" />
</shape>
</item>
</selector>
<!-- wspólny wygląd dwunastu klawiszy -->
<style name="Klawisz">
<item name="android:layout_width">72dp</item>
<item name="android:layout_height">72dp</item>
<item name="android:layout_margin">8dp</item>
<item name="android:background">@drawable/tlo_klawisz</item>
<item name="android:textColor">@color/tekst_klawisz</item>
<item name="android:textSize">26sp</item>
</style>
<!-- klawisz kasowania: wszystko jak Klawisz, ale czerwony znak -->
<style name="Klawisz.Usun">
<item name="android:textColor">#C62828</item>
</style>
<?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:fitsSystemWindows="true"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Wpisz PIN"
android:textSize="24sp" />
<!-- cztery kropki -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<View android:id="@+id/kropka1" android:layout_width="16dp" android:layout_height="16dp"
android:layout_margin="8dp" android:background="@drawable/kropka" />
<View android:id="@+id/kropka2" android:layout_width="16dp" android:layout_height="16dp"
android:layout_margin="8dp" android:background="@drawable/kropka" />
<View android:id="@+id/kropka3" android:layout_width="16dp" android:layout_height="16dp"
android:layout_margin="8dp" android:background="@drawable/kropka" />
<View android:id="@+id/kropka4" android:layout_width="16dp" android:layout_height="16dp"
android:layout_margin="8dp" android:background="@drawable/kropka" />
</LinearLayout>
<TextView
android:id="@+id/tvKomunikat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:minHeight="24dp"
android:textSize="16sp" />
<!-- cztery rzędy po trzy klawisze; rozmiar, tło i kolor z jednego stylu -->
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn1" style="@style/Klawisz" android:text="1" />
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn2" style="@style/Klawisz" android:text="2" />
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn3" style="@style/Klawisz" android:text="3" />
</LinearLayout>
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn4" style="@style/Klawisz" android:text="4" />
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn5" style="@style/Klawisz" android:text="5" />
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn6" style="@style/Klawisz" android:text="6" />
</LinearLayout>
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn7" style="@style/Klawisz" android:text="7" />
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn8" style="@style/Klawisz" android:text="8" />
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn9" style="@style/Klawisz" android:text="9" />
</LinearLayout>
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btnUsun" style="@style/Klawisz.Usun"
android:contentDescription="Usuń ostatnią cyfrę" android:text="⌫" />
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btn0" style="@style/Klawisz" android:text="0" />
<androidx.appcompat.widget.AppCompatButton android:id="@+id/btnOk" style="@style/Klawisz"
android:text="OK" android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
package pl.schoolit.pin
import android.graphics.Color
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import android.widget.Button
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 tvKomunikat = findViewById<TextView>(R.id.tvKomunikat)
val btnUsun = findViewById<Button>(R.id.btnUsun)
val btnOk = findViewById<Button>(R.id.btnOk)
// dziesięć klawiszy cyfr i cztery kropki w tablicach — obsłużymy je pętlą
val cyfry = arrayOf(
findViewById<Button>(R.id.btn0), findViewById<Button>(R.id.btn1),
findViewById<Button>(R.id.btn2), findViewById<Button>(R.id.btn3),
findViewById<Button>(R.id.btn4), findViewById<Button>(R.id.btn5),
findViewById<Button>(R.id.btn6), findViewById<Button>(R.id.btn7),
findViewById<Button>(R.id.btn8), findViewById<Button>(R.id.btn9)
)
val kropki = arrayOf(
findViewById<View>(R.id.kropka1), findViewById<View>(R.id.kropka2),
findViewById<View>(R.id.kropka3), findViewById<View>(R.id.kropka4)
)
val poprawnyPin = "2580"
var pin = ""
var bledy = 0
fun pokazStan() {
// kropka nr i jest „wybrana”, gdy wpisano więcej niż i cyfr — selektor zrobi resztę
for (i in kropki.indices) {
kropki[i].isSelected = i < pin.length
}
btnUsun.isEnabled = pin.isNotEmpty()
btnOk.isEnabled = pin.length == 4
}
fun ustawKlawiature(wlaczona: Boolean) {
for (k in cyfry) {
k.isEnabled = wlaczona
}
if (wlaczona) {
pokazStan() // Usuń i OK zależą od długości PIN-u
} else {
btnUsun.isEnabled = false
btnOk.isEnabled = false
}
}
pokazStan()
// jeden kod dla wszystkich cyfr: cyfrę bierzemy z napisu klawisza
for (k in cyfry) {
k.setOnClickListener {
if (pin.length < 4) {
pin += k.text
tvKomunikat.text = ""
pokazStan()
}
}
}
btnUsun.setOnClickListener {
pin = pin.dropLast(1) // napis bez ostatniego znaku
pokazStan()
}
btnOk.setOnClickListener {
if (pin == poprawnyPin) {
tvKomunikat.text = "Odblokowano!"
tvKomunikat.setTextColor(Color.parseColor("#2E7D32"))
ustawKlawiature(false)
return@setOnClickListener
}
bledy++
pin = ""
pokazStan()
tvKomunikat.setTextColor(Color.parseColor("#C62828"))
if (bledy < 3) {
tvKomunikat.text = "Błędny PIN. Pozostało prób: ${3 - bledy}"
return@setOnClickListener
}
// trzeci błąd: blokada na 10 sekund
ustawKlawiature(false)
object : CountDownTimer(10_000, 1000) {
override fun onTick(pozostalo: Long) {
tvKomunikat.text = "Zablokowano. Spróbuj za ${pozostalo / 1000 + 1} s"
}
override fun onFinish() {
bledy = 0
tvKomunikat.text = ""
ustawKlawiature(true)
}
}.start()
}
}
}
Co dzieje się po kolei
- Każdy klawisz w XML ma tylko
id,stylei tekst. Rozmiar, margines, tło i kolor przychodzą ze styluKlawisz. Klawisz kasowania używaKlawisz.Usun— tego samego wyglądu z czerwonym znakiem. - Tło klawisza to selektor: fioletowe koło przy naciśnięciu, szare przy wyłączeniu, jasne w pozostałych przypadkach. Kolor tekstu zmienia drugi selektor — z folderu
res/color. - Klawisze cyfr są w tablicy, a
setOnClickListenerustawiamy w pętli. Cyfrę bierzemy z napisu klikniętego klawisza —k.text. - Kropki to zwykłe
Viewz selektorem. Kod przestawia tylkoisSelected, a selektor podmienia obrys na pełne koło. - Po trzecim błędzie
ustawKlawiature(false)wyłącza wszystko. Szary wygląd to zasługa selektora (state_enabled="false"), nie kodu. pin.dropLast(1)zwraca napis bez ostatniego znaku — tak działa kasowanie.
Sprawdź trzy rzeczy
- Przytrzymaj palec na klawiszu — tło jest fioletowe, a cyfra biała, dopóki go nie puścisz.
- Przenieś w
tlo_klawisz.xmlostatni<item>(bez warunku) na samą górę. Klawisze przestaną reagować wyglądem na dotyk i blokadę. - Zmień w stylu
Klawiszrozmiar na 64dp. Zmienią się wszystkie klawisze naraz.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<selector> + <item android:state_…> | drawable / color | Wygląd zależny od stanu; pierwszy pasujący wygrywa. |
state_pressed, state_enabled, state_checked, state_selected | stany | Naciśnięty, włączony, zaznaczony, wybrany. |
res/color/…xml | zasób | Selektor koloru (android:color). |
view.isSelected | Boolean | Stan „wybrany” ustawiany z kodu. |
<style name="…"><item name="…"> | zasób | Zestaw atrybutów pod jedną nazwą. |
style="@style/…" | atrybut | Użycie stylu w kontrolce. |
Nazwa.Potomny, parent="…" | dziedziczenie | Styl przejmujący atrybuty innego. |
colorPrimary, colorOnPrimary | motyw | Kolor główny aplikacji i tekstu na nim. |
napis.dropLast(n) | String | Napis bez n ostatnich znaków. |
for (k in tablica) k.setOnClickListener { } | pętla | Ten sam listener dla wielu kontrolek. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| selektor zawsze pokazuje stan domyślny | Element bez warunku stoi przed innymi. Przenieś go na koniec. |
selektor koloru w res/drawable | Błąd przy textColor: …is not a color. Selektory kolorów idą do res/color. |
android:style="@style/Klawisz" | Styl jest ignorowany. Atrybut style pisze się bez przedrostka android:. |
styl na <Button> — tło się nie zmienia | Przycisk Material przemalowuje tło. Używaj AppCompatButton (lekcja 17). |
| kolor motywu zmienia się tylko w trybie jasnym | Tryb ciemny ma osobny values-night/themes.xml. |
isSelected = true nic nie zmienia | Tło kontrolki nie jest selektorem ze stanem state_selected. |
| klawisze nie mieszczą się na małym ekranie | Stały rozmiar 72dp w stylu. Zmniejsz go w jednym miejscu — w stylu. |
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.
Przycisk w kształcie pigułki z ramką: w spoczynku biały z fioletową ramką i fioletowym tekstem, po naciśnięciu fioletowy z białym tekstem, wyłączony — szary. Drugi przycisk włącza i wyłącza pierwszy.
pokaż wskazówkę
- Dwa selektory: tła w
drawablei tekstu wcolor. Przycisk:AppCompatButton.
Trzy pola wyboru z własnym wyglądem: zamiast kwadracika — pigułka na całą szerokość, szara gdy niezaznaczona, zielona z białym tekstem gdy zaznaczona.
pokaż wskazówkę
- Selektor ze stanem
state_checkedjakoandroid:background, a domyślny kwadracik ukryj atrybutemandroid:button="@null".
Siedem małych kafelków z literami P W Ś C P S N w jednym rzędzie. Dotknięcie kafelka czyni go wybranym (pełny kolor), a poprzednio wybrany wraca do zwykłego wyglądu. Pod spodem napis z pełną nazwą dnia. Wszystkie kafelki korzystają z jednego stylu.
pokaż wskazówkę
- Selektor z
state_selected. W listenerze pętlą ustawisSelected = falsewszystkim, a potemtrueklikniętemu.
Ekran z nagłówkiem, trzema akapitami tekstu i dwoma przypisami. Zdefiniuj styl Tekst (rozmiar, kolor, odstęp linii) i trzy potomne: Tekst.Naglowek (większy, pogrubiony), Tekst.Akapit (wcięcie pierwszej linii — margines), Tekst.Przypis (mniejszy, kursywa, szary). Zmiana koloru w stylu Tekst ma zmienić wszystkie napisy.
pokaż wskazówkę
- Styl potomny przez kropkę w nazwie dziedziczy wszystkie atrybuty — dopisuj w nim tylko to, co inne.
Weź dowolny projekt z rozdziałów 2–4 (np. kasę kina z lekcji 10) i zmień jego motyw na kolory szkoły: colorPrimary, colorOnPrimary, colorSecondary. Zmiany mają objąć tryb jasny i ciemny. Nie zmieniaj żadnego pliku układu.
pokaż wskazówkę
- Dwa pliki:
values/themes.xmlivalues-night/themes.xml. Tryb ciemny na emulatorze: Ustawienia → Wyświetlacz → Ciemny motyw.
Kalkulator czterech działań na liczbach całkowitych. Trzy style klawiszy: Klawisz.Cyfra, Klawisz.Dzialanie (inny kolor tła — osobny selektor), Klawisz.Rownasie (szerszy). Klawisze działań są wyłączone, dopóki nie wpisano pierwszej liczby; po wybraniu działania jego klawisz pozostaje „wybrany” (state_selected) aż do wpisania drugiej liczby.
pokaż wskazówkę
- Stan kalkulatora:
var liczba1,var dzialanie: String?,var biezaca = "". Cyfry i działania obsłuż pętlami po tablicach — jak w przykładzie.
Co trzeba zapamiętać
- Selektor wybiera wygląd według stanu; element bez warunku zawsze na końcu.
- Selektory tła — w
res/drawable, selektory kolorów — wres/color. isSelectediisEnabledz kodu + selektor = zmiana wyglądu bez podmieniania tła.- Styl zbiera wspólne atrybuty;
style="@style/…"bezandroid:; dziedziczenie przez kropkę. - Motyw w
themes.xmlzmienia kolory całej aplikacji — pamiętaj ovalues-night.
Dokumentacja: Selektor (StateListDrawable), Selektor kolorów, Style i motywy.