school-it.pl // android + kotlin // rozdział 2 // lekcja 05
Button i obsługa kliknięcia
Do tej pory aplikacja coś pokazywała, ale nie reagowała na użytkownika. Button to przycisk, a setOnClickListener to sposób, żeby po jego kliknięciu wykonał się twój kod. W tej lekcji zbudujesz licznik osób wchodzących do sali: kilka przycisków, zmienna, która pamięta stan, i przyciski, które same się wyłączają, gdy nie wolno ich już używać.
Czego się dziś nauczysz
- Dodasz przycisk w XML i zmienisz jego wygląd.
- Obsłużysz kliknięcie metodą
setOnClickListener. - Zapamiętasz stan aplikacji w zmiennej zmienianej przez przyciski.
- Wyłączysz przycisk, którego chwilowo nie wolno użyć (
isEnabled). - Odróżnisz zwykłe kliknięcie od długiego przytrzymania.
Przycisk w XML
Przycisk dodajesz tak jak napis — ma id, rozmiar i tekst. Motyw projektu (Material 3) nadaje mu zaokrąglony kształt i kolor. Wygląd zmienisz atrybutami albo gotowym stylem:
<!-- zwykły, wypełniony kolorem motywu -->
<Button
android:id="@+id/btnZapisz"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Zapisz" />
<!-- inny kolor tła: backgroundTint (nie background!) -->
<Button
android:id="@+id/btnUsun"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="#C62828"
android:text="Usuń" />
<!-- gotowy styl: obramowanie bez wypełnienia -->
<Button
android:id="@+id/btnAnuluj"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Anuluj" />
android:background na przycisku nie działa
W motywie Material 3 przycisk sam zarządza swoim tłem i ignoruje android:background. Kolor zmieniaj przez android:backgroundTint. Całkowicie własny kształt tła poznasz w lekcji 17.
Tekst przycisku Material 3 jest wyświetlany tak, jak go wpiszesz. Gdy chcesz wielkie litery, dodaj android:textAllCaps="true". Przycisk ma co najmniej 48 dp wysokości — to minimalny rozmiar celu dla palca.
Reakcja na kliknięcie
Żeby przycisk coś robił, w onCreate znajdujesz go i dajesz mu kod do wykonania po kliknięciu:
val btnPowitanie = findViewById<Button>(R.id.btnPowitanie)
val tvInfo = findViewById<TextView>(R.id.tvInfo)
btnPowitanie.setOnClickListener {
tvInfo.text = "Cześć! Kliknąłeś przycisk." // wykona się PO kliknięciu
}
Kod w klamrach { … } to lambda — kawałek kodu przekazany do metody, żeby uruchomiła go później. setOnClickListener nie wykonuje go od razu. Zapamiętuje go i oddaje Androidowi, a Android uruchamia go przy każdym kliknięciu.
„zapamiętaj ten kod”
na ekran
kod z klamer
Każdy przycisk ma własny setOnClickListener. Kod w klamrach może wszystko, co zwykły kod: zmieniać napisy, liczyć, sprawdzać warunki, zmieniać inne przyciski.
A atrybut android:onClick?
W starszych materiałach zobaczysz w XML android:onClick="kliknij" i metodę fun kliknij(view: View) w aktywności. To przestarzały sposób: literówka w nazwie wychodzi dopiero przy kliknięciu, jako awaria. W kursie używamy wyłącznie setOnClickListener.
Zmienna, która pamięta
Licznik kliknięć musi gdzieś trzymać liczbę. Deklarujesz zmienną var w onCreate, przed setOnClickListener. Kod w klamrach widzi ją i może zmieniać — liczba przetrwa między kliknięciami:
var klikniecia = 0 // raz, przy starcie ekranu
btnKlik.setOnClickListener {
klikniecia++ // przy każdym kliknięciu +1
tvInfo.text = "Kliknięcia: $klikniecia"
}
Zmienna w klamrach zaczyna od zera za każdym razem
Gdy przeniesiesz var klikniecia = 0 do środka klamer, licznik zawsze pokaże 1: przy każdym kliknięciu powstaje nowa zmienna z wartością 0, rośnie do 1 i znika po wykonaniu kodu.
Zmiana przycisku z kodu
| Zapis | Efekt |
|---|---|
btn.isEnabled = false | Przycisk szarzeje i nie reaguje na kliknięcia. true — znów działa. |
btn.text = "Stop" | Zmienia napis na przycisku. |
btn.isVisible = false | Ukrywa przycisk; miejsce po nim się zwalnia. Wymaga import androidx.core.view.isVisible. |
btn.backgroundTintList = ColorStateList.valueOf(Color.RED) | Zmienia kolor tła przycisku (rzadziej potrzebne). |
Wyłączanie przycisków jest lepsze niż komunikat „nie można”. Jeśli licznik jest na zerze, przycisk − po prostu nie działa i to widać — użytkownik nie musi klikać, żeby się dowiedzieć.
Długie przytrzymanie
Akcje, które trudno cofnąć, można ukryć pod długim przytrzymaniem palca. Służy do tego setOnLongClickListener. Jego klamry muszą na końcu zwrócić true — „obsłużyłem przytrzymanie, nie traktuj go już jako zwykłego kliknięcia”:
btnZeruj.setOnLongClickListener {
licznik = 0
tvInfo.text = "Wyzerowano"
true // ostatnia linia: wynik lambdy
}
Licznik osób w sali
Na szkolny koncert do sali wchodzi najwyżej 10 osób. Dyżurny klika + przy każdym wejściu i − przy wyjściu. Aplikacja pokazuje stan „7 / 10” i liczbę wolnych miejsc. Gdy sala jest pełna, licznik robi się czerwony, a + się wyłącza. − nie działa przy zerze. Zerowanie wymaga przytrzymania przycisku — zwykłe kliknięcie tylko podpowiada, co zrobić.
wszystkie trzy ekrany to ta sama aplikacja — zmienia się tylko wartość zmiennej osoby
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
LicznikSali, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt. Teksty są wpisane w XML wprost, żeby przykład był krótszy — w zadaniu 1 przeniesiesz je dostrings.xml.
<?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:gravity="center_horizontal"
android:layout_margin="16dp"
android:fitsSystemWindows="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Koncert — sala 12"
android:textSize="22sp" />
<TextView
android:id="@+id/tvLicznik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="0 / 10"
android:textSize="72sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wolnych miejsc: 10"
android:textSize="18sp" />
<!-- dwa równe przyciski w wierszu -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="horizontal">
<Button
android:id="@+id/btnMinus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="−"
android:textSize="28sp" />
<Button
android:id="@+id/btnPlus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="+"
android:textSize="28sp" />
</LinearLayout>
<Button
android:id="@+id/btnZeruj"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Zeruj (przytrzymaj)" />
</LinearLayout>
package pl.schoolit.liczniksali
import android.graphics.Color
import android.os.Bundle
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)
// kontrolki
val tvLicznik = findViewById<TextView>(R.id.tvLicznik)
val tvInfo = findViewById<TextView>(R.id.tvInfo)
val btnMinus = findViewById<Button>(R.id.btnMinus)
val btnPlus = findViewById<Button>(R.id.btnPlus)
val btnZeruj = findViewById<Button>(R.id.btnZeruj)
// stan aplikacji
val limit = 10
var osoby = 0
// pokazuje aktualny stan — wywołujemy po każdej zmianie liczby osób,
// żeby nie przepisywać tych samych linii w trzech przyciskach
fun pokazStan() {
tvLicznik.text = "$osoby / $limit"
btnMinus.isEnabled = osoby > 0
btnPlus.isEnabled = osoby < limit
if (osoby == limit) {
tvLicznik.setTextColor(Color.parseColor("#C62828"))
tvInfo.text = "Sala pełna!"
} else {
tvLicznik.setTextColor(Color.BLACK)
tvInfo.text = "Wolnych miejsc: ${limit - osoby}"
}
}
pokazStan() // stan początkowy: − wyłączony
btnPlus.setOnClickListener {
osoby++
pokazStan()
}
btnMinus.setOnClickListener {
osoby--
pokazStan()
}
// zwykłe kliknięcie: tylko podpowiedź
btnZeruj.setOnClickListener {
tvInfo.text = "Przytrzymaj przycisk, aby wyzerować"
}
// przytrzymanie: zerowanie
btnZeruj.setOnLongClickListener {
osoby = 0
pokazStan()
true
}
}
}
Co dzieje się po kolei
- Na początku
onCreateznajdujemy pięć kontrolek, a potem tworzymy zmiennąosoby— to jedyne miejsce, gdzie aplikacja pamięta liczbę osób. pokazStan()to funkcja zapisana w środkuonCreate. Zbiera w jednym miejscu wszystko, co trzeba odświeżyć po zmianie liczby: napis licznika, kolory i włączenie przycisków. Bez niej te same 9 linii trzeba by powtórzyć w trzech przyciskach.btnMinus.isEnabled = osoby > 0— warunek dajetruealbofalsei to od razu jest stan przycisku. Nie trzeba pisaćif.- Wywołanie
pokazStan()przed ustawieniem przycisków ustawia ekran startowy: przy zerze − jest wyłączony. - Przyciski + i − zmieniają tylko liczbę i proszą o odświeżenie ekranu. Nie muszą sprawdzać granic — wyłączony przycisk w ogóle nie wywoła swojego kodu.
btnZerujma dwa słuchacze: kliknięcie pokazuje podpowiedź, przytrzymanie zeruje.truena końcu przytrzymania sprawia, że po puszczeniu palca nie wykona się już zwykłe kliknięcie.
onCreate robi się długie
W tym przykładzie onCreate ma już ponad 50 linii, a są w nim tylko trzy przyciski. W większych aplikacjach to szybko staje się nieczytelne. W lekcji 19 poznasz sposób porządkowania takiego kodu — na razie celowo trzymamy wszystko w jednym miejscu.
Sprawdź trzy rzeczy
- Klikaj + do 10. Za dziesiątym razem licznik zrobi się czerwony, a + szary. Kliknij − — kolor wróci, a + znów zadziała.
- Krótko kliknij Zeruj, potem go przytrzymaj. Pierwsze tylko podpowiada, drugie zeruje.
- Obróć emulator przy stanie 7. Licznik wróci do 0 — Android tworzy ekran od nowa, a
var osoby = 0wykonuje się ponownie. Jak tego uniknąć, dowiesz się w lekcjach 36 i 40.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<Button> | kontrolka | Przycisk z tekstem. |
android:backgroundTint | atrybut | Kolor tła przycisku. |
style="@style/Widget.Material3.Button.OutlinedButton" | styl | Przycisk z obramowaniem. Inne: …TextButton, …TonalButton. |
android:enabled="false" | atrybut | Przycisk wyłączony od startu. |
btn.setOnClickListener { } | metoda | Kod wykonywany po kliknięciu. |
btn.setOnLongClickListener { …; true } | metoda | Kod po przytrzymaniu; zwraca true. |
btn.isEnabled | Boolean | Czy przycisk działa. |
btn.text | właściwość | Napis na przycisku. |
var przed listenerem | zmienna | Stan, który przetrwa między kliknięciami. |
fun nazwa() { } w onCreate | funkcja lokalna | Kod używany przez kilka przycisków. |
Color.BLACK, Color.parseColor | Int | Kolory (lekcja 04). |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| licznik zawsze pokazuje 1 | var licznik = 0 jest wewnątrz klamer. Przenieś go przed setOnClickListener. |
val licznik = 0, potem licznik++ | Błąd kompilacji: Val cannot be reassigned. Zmienną, która się zmienia, deklaruj przez var. |
| przycisk nie reaguje, brak błędów | Brak setOnClickListener, albo ustawiony na innym przycisku (pomylone R.id). |
android:background="#C62828" nie zmienia koloru | Motyw Material ignoruje background. Użyj backgroundTint. |
| przytrzymanie zeruje, a potem jeszcze pojawia się podpowiedź | Lambda setOnLongClickListener zwraca false — kliknięcie obsługuje się drugi raz. Ostatnia linia: true. |
| licznik spada poniżej zera | Przycisk − nie jest wyłączany. Ustaw btnMinus.isEnabled = osoby > 0 po każdej zmianie. |
findViewById<Button>(R.id.tvLicznik) | Awaria ClassCastException: pod tym id jest TextView, a nie Button. |
Zadania
Zadanie 1 wykonaj w projekcie z lekcji, pozostałe w nowych projektach Empty Views Activity. Cały kod pisz w onCreate. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Przenieś wszystkie teksty z activity_main.xml do strings.xml. Zmień limit na 25 i sprawdź, że wystarczy zmiana w jednym miejscu kodu. Dodaj przycisk +5 (dla grupy), który wyłącza się, gdy zostało mniej niż 5 wolnych miejsc.
pokaż wskazówkę
- W
pokazStan()dopiszbtnPlus5.isEnabled = osoby + 5 <= limit.
Jeden przycisk na środku ekranu. Kliknięcie przełącza „światło”: tło całego ekranu zmienia się z czarnego na żółte i z powrotem, a napis przycisku z „Włącz” na „Wyłącz”. Pod przyciskiem napis „Przełączono 7 razy”.
pokaż wskazówkę
- Stan trzymaj w
var wlaczone = false, a przełączaj przezwlaczone = !wlaczone. Tło ekranu zmienisz, znajdując główny układ:findViewById<LinearLayout>(R.id.main).setBackgroundColor(…).
Przycisk Rzuć losuje liczbę 1–6 i pokazuje ją dużą cyfrą. Pod nią: „Rzutów: 12, suma: 41, średnia: 3,42”. Szóstka pokazuje się na zielono. Przycisk Od nowa zeruje statystyki.
pokaż wskazówkę
- Średnią licz jako
suma.toDouble() / rzuty, a formatuj przezString.format("%.2f", srednia). Przy zerze rzutów nie dziel.
Ankieta „Gdzie jedziemy na wycieczkę?” z trzema przyciskami: Góry, Morze, Jezioro. Każdy ma pod sobą liczbę głosów. Napis na dole pokazuje aktualnego lidera albo „Remis”. Po 20 głosach wszystkie przyciski się wyłączają i pojawia się „Głosowanie zakończone: wygrywa Morze”.
pokaż wskazówkę
- Trzy zmienne
vari jedna funkcjapokazWyniki()wywoływana przez każdy przycisk — jakpokazStan()w przykładzie.
Aplikacja losuje liczbę 1–100. Użytkownik ustawia swoją propozycję przyciskami −10, −1, +1, +10 (start od 50, nie można wyjść poza 1–100 — przyciski się wyłączają) i klika Sprawdź. Aplikacja odpowiada „Za mało”, „Za dużo” albo „Trafiłeś w 6 próbach!”. Po trafieniu Sprawdź zmienia się w Nowa gra.
pokaż wskazówkę
- Jeden przycisk z dwiema rolami: w
setOnClickListenersprawdź zmiennąvar koniecGryi zależnie od niej sprawdzaj albo zaczynaj od nowa.
Aplikacja do mierzenia czasu okrążeń na WF bez zegara: przycisk Start zapisuje czas rozpoczęcia, przycisk Okrążenie dopisuje do listy (jeden TextView z przewijaniem) „Okrążenie 3: 52,4 s”, przycisk Stop kończy pomiar i pokazuje najlepsze okrążenie. Przyciski są włączone tylko wtedy, gdy mają sens (np. Okrążenie tylko po Start).
pokaż wskazówkę
- Aktualny czas w milisekundach:
System.currentTimeMillis(). Czas okrążenia to różnica między teraz a poprzednim kliknięciem. Najlepsze okrążenie trzymaj wvar najlepsze = Long.MAX_VALUE.
Co trzeba zapamiętać
- Kliknięcie obsługujesz przez
przycisk.setOnClickListener { … }— kod w klamrach wykona się dopiero po kliknięciu. - Stan aplikacji trzymaj w
varzadeklarowanym przed listenerami. isEnabled = falsewyłącza przycisk — lepsze niż komunikat o błędzie.- Kod powtarzany w kilku przyciskach zbierz w jednej funkcji i wywołuj ją po każdej zmianie.
- Kolor tła przycisku zmieniaj przez
backgroundTint; przytrzymanie tosetOnLongClickListenerzakończonytrue.
Dokumentacja: Przyciski, Przyciski Material 3.