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

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

Kotlin XML Button setOnClickListener lambda isEnabled setOnLongClickListener 2 × 45 min
CEL LEKCJI

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

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:

trzy przyciski — trzy style
<!-- 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" />
12:30
Zapisz
Usuń
Anuluj
Wyłączony (enabled=”false”)
przyciski w motywie Material 3

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.

TEORIA

Reakcja na kliknięcie

Żeby przycisk coś robił, w onCreate znajdujesz go i dajesz mu kod do wykonania po kliknięciu:

MainActivity.kt — fragment onCreate
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.

onCreate
setOnClickListener { … }
„zapamiętaj ten kod”
→
aplikacja czeka
użytkownik patrzy
na ekran
→
kliknięcie
Android uruchamia
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.

TEORIA

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:

licznik kliknięć
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.

TEORIA

Zmiana przycisku z kodu

ZapisEfekt
btn.isEnabled = falsePrzycisk szarzeje i nie reaguje na kliknięcia. true — znów działa.
btn.text = "Stop"Zmienia napis na przycisku.
btn.isVisible = falseUkrywa 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ć.

TEORIA

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”:

przytrzymanie
btnZeruj.setOnLongClickListener {
    licznik = 0
    tvInfo.text = "Wyzerowano"
    true                                  // ostatnia linia: wynik lambdy
}
PRZYKŁAD

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

18:00
Koncert — sala 12
0 / 10
Wolnych miejsc: 10
−
+
Zeruj (przytrzymaj)
start: przycisk − wyłączony
18:12
Koncert — sala 12
7 / 10
Wolnych miejsc: 3
−
+
Zeruj (przytrzymaj)
siedem kliknięć +
18:20
Koncert — sala 12
10 / 10
Sala pełna!
−
+
Zeruj (przytrzymaj)
limit: czerwony licznik, + wyłączony

wszystkie trzy ekrany to ta sama aplikacja — zmienia się tylko wartość zmiennej osoby

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa LicznikSali, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt. Teksty są wpisane w XML wprost, żeby przykład był krótszy — w zadaniu 1 przeniesiesz je do strings.xml.
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: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>
MainActivity.kt — pełny plik
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 onCreate znajdujemy pięć kontrolek, a potem tworzymy zmienną osoby — to jedyne miejsce, gdzie aplikacja pamięta liczbę osób.
  • pokazStan() to funkcja zapisana w środku onCreate. 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 daje true albo false i 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.
  • btnZeruj ma dwa słuchacze: kliknięcie pokazuje podpowiedź, przytrzymanie zeruje. true na 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

  1. Klikaj + do 10. Za dziesiątym razem licznik zrobi się czerwony, a + szary. Kliknij − — kolor wróci, a + znów zadziała.
  2. Krótko kliknij Zeruj, potem go przytrzymaj. Pierwsze tylko podpowiada, drugie zeruje.
  3. Obróć emulator przy stanie 7. Licznik wróci do 0 — Android tworzy ekran od nowa, a var osoby = 0 wykonuje się ponownie. Jak tego uniknąć, dowiesz się w lekcjach 36 i 40.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<Button>kontrolkaPrzycisk z tekstem.
android:backgroundTintatrybutKolor tła przycisku.
style="@style/Widget.Material3.Button.OutlinedButton"stylPrzycisk z obramowaniem. Inne: …TextButton, …TonalButton.
android:enabled="false"atrybutPrzycisk wyłączony od startu.
btn.setOnClickListener { }metodaKod wykonywany po kliknięciu.
btn.setOnLongClickListener { …; true }metodaKod po przytrzymaniu; zwraca true.
btn.isEnabledBooleanCzy przycisk działa.
btn.textwłaściwośćNapis na przycisku.
var przed listeneremzmiennaStan, który przetrwa między kliknięciami.
fun nazwa() { } w onCreatefunkcja lokalnaKod używany przez kilka przycisków.
Color.BLACK, Color.parseColorIntKolory (lekcja 04).
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
licznik zawsze pokazuje 1var 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ówBrak setOnClickListener, albo ustawiony na innym przycisku (pomylone R.id).
android:background="#C62828" nie zmienia koloruMotyw 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 zeraPrzycisk − 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

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.

ZAD 1Teksty do zasobów, limit ze stałej★☆☆

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() dopisz btnPlus5.isEnabled = osoby + 5 <= limit.
ZAD 2Włącznik światła★☆☆

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 przez wlaczone = !wlaczone. Tło ekranu zmienisz, znajdując główny układ: findViewById<LinearLayout>(R.id.main).setBackgroundColor(…).
ZAD 3Rzut kostką★☆☆

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 przez String.format("%.2f", srednia). Przy zerze rzutów nie dziel.
ZAD 4Głosowanie★★☆

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 var i jedna funkcja pokazWyniki() wywoływana przez każdy przycisk — jak pokazStan() w przykładzie.
ZAD 5Zgadnij liczbę — wersja przyciskowa★★☆

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 setOnClickListener sprawdź zmienną var koniecGry i zależnie od niej sprawdzaj albo zaczynaj od nowa.
ZAD 6Stoper rundowy★★★

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 w var najlepsze = Long.MAX_VALUE.
PODSUMOWANIE

Co trzeba zapamiętać

  • Kliknięcie obsługujesz przez przycisk.setOnClickListener { … } — kod w klamrach wykona się dopiero po kliknięciu.
  • Stan aplikacji trzymaj w var zadeklarowanym przed listenerami.
  • isEnabled = false wyłą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 to setOnLongClickListener zakończony true.

Dokumentacja: Przyciski, Przyciski Material 3.

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