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

school-it.pl // android + kotlin // rozdział 3 // lekcja 09

CheckBox — wiele opcji

CheckBox to pole wyboru: kwadracik, który można zaznaczyć albo odznaczyć. Każdy działa niezależnie, więc nadaje się wszędzie tam, gdzie użytkownik wybiera dowolnie wiele opcji naraz — dodatki do pizzy, zainteresowania, zgody w formularzu. W tej lekcji zbudujesz zamówienie pizzy, w którym cena przelicza się na bieżąco przy każdym kliknięciu.

Kotlin XML CheckBox isChecked setOnCheckedChangeListener 1 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Dodasz pola wyboru i ustawisz ich stan początkowy.
  • Odczytasz, czy pole jest zaznaczone (isChecked).
  • Zaznaczysz i odznaczysz pole z kodu.
  • Zareagujesz na każdą zmianę zaznaczenia.
  • Policzysz sumę i zbudujesz listę wybranych opcji.
TEORIA

CheckBox w XML

dwa pola wyboru
<CheckBox
    android:id="@+id/cbSer"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Podwójny ser (+4 zł)" />

<CheckBox
    android:id="@+id/cbRegulamin"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:checked="true"
    android:text="Akceptuję regulamin" />

android:text to podpis obok kwadracika. Kliknięcie w podpis też przełącza pole — cały wiersz jest celem dotyku. android:checked="true" zaznacza pole na starcie.

12:30
Podwójny ser (+4 zł)
Akceptuję regulamin
Wyłączony (enabled=”false”)
niezaznaczony, zaznaczony, wyłączony
TEORIA

Odczyt i zmiana stanu

Stan pola to właściwość isChecked typu Boolean. Można ją czytać i zmieniać:

MainActivity.kt — fragment onCreate
val cbRegulamin = findViewById<CheckBox>(R.id.cbRegulamin)

btnWyslij.setOnClickListener {
    if (cbRegulamin.isChecked) {            // odczyt
        tvInfo.text = "Wysłano"
    } else {
        tvInfo.text = "Zaakceptuj regulamin"
    }
}

btnWyczysc.setOnClickListener {
    cbRegulamin.isChecked = false           // zmiana z kodu
}

Pole można też przełączyć metodą toggle() — zaznaczone staje się odznaczone i odwrotnie.

TEORIA

Reakcja na zmianę

Czasem stan pola odczytujesz dopiero po kliknięciu przycisku. Częściej jednak ekran ma reagować od razu — na przykład cena ma się zmienić w chwili zaznaczenia dodatku. Do tego służy setOnCheckedChangeListener:

reakcja na każdą zmianę
cbRegulamin.setOnCheckedChangeListener { _, zaznaczony ->
    btnWyslij.isEnabled = zaznaczony        // przycisk aktywny tylko po akceptacji
}

Android przekazuje do klamer dwie wartości: pole, które się zmieniło (tu niepotrzebne — _), i jego nowy stan (zaznaczony). Kod wykonuje się przy każdej zmianie — także wtedy, gdy zmienisz isChecked z kodu.

setOnClickListener a setOnCheckedChangeListener

Na polu wyboru zadziała też setOnClickListener, ale tylko dla kliknięć palcem. setOnCheckedChangeListener wywoła się przy każdej zmianie stanu — także z kodu, np. po przycisku „Wyczyść”. Dlatego do pól wyboru używamy tego drugiego.

TEORIA

Wiele pól naraz

Przy kilku polach zwykle trzeba policzyć coś na podstawie wszystkich — sumę, liczbę zaznaczonych, listę nazw. Wygodnie jest napisać jedną funkcję, która to robi, i wywołać ją z listenera każdego pola. Każde pole ma przy tym ten sam kod w klamrach:

jedna funkcja, wiele pól
fun przelicz() {
    var suma = 0
    if (cbSer.isChecked) suma += 4
    if (cbSzynka.isChecked) suma += 5
    tvSuma.text = "Dodatki: $suma zł"
}

cbSer.setOnCheckedChangeListener { _, _ -> przelicz() }
cbSzynka.setOnCheckedChangeListener { _, _ -> przelicz() }

Gdy if ma tylko jedną instrukcję, można zapisać go w jednej linii bez klamer: if (warunek) suma += 4.

PRZYKŁAD

Zamówienie pizzy

Pizza bazowa kosztuje 24 zł. Sześć dodatków ma różne ceny. Cena końcowa i lista dodatków zmieniają się w chwili zaznaczenia lub odznaczenia pola. Przy więcej niż czterech dodatkach pojawia się rabat 3 zł. Przycisk Bez dodatków odznacza wszystko, a Zamów jest aktywny tylko po zaznaczeniu zgody na regulamin.

19:05
Pizza Margherita · 24 zł
Dodatki
Podwójny ser (+4 zł)
Szynka (+5 zł)
Pieczarki (+3 zł)
Oliwki (+3 zł)
Jalapeño (+2 zł)
Ananas (+4 zł)
Bez dodatków
Do zapłaty: 24 zł
Akceptuję regulamin
Zamów
start: nic nie zaznaczone, Zamów wyłączony
→zaznaczenie
3 dodatków
i regulaminu
19:06
Pizza Margherita · 24 zł
Dodatki
Podwójny ser (+4 zł)
Szynka (+5 zł)
Pieczarki (+3 zł)
Oliwki (+3 zł)
Jalapeño (+2 zł)
Ananas (+4 zł)
ser, pieczarki, jalapeño
Do zapłaty: 33 zł
Akceptuję regulamin
Zamów
cena i lista zmieniają się na bieżąco

24 zł + 4 + 3 + 2 = 33 zł; przy pięciu i więcej dodatkach cena spada o 3 zł

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Pizza, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt. Formularz jest w ScrollView — na małym ekranie dół trzeba przewinąć.
res/layout/activity_main.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Pizza Margherita · 24 zł"
            android:textSize="22sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:text="Dodatki"
            android:textStyle="bold" />

        <CheckBox android:id="@+id/cbSer"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:text="Podwójny ser (+4 zł)" />
        <CheckBox android:id="@+id/cbSzynka"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:text="Szynka (+5 zł)" />
        <CheckBox android:id="@+id/cbPieczarki"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:text="Pieczarki (+3 zł)" />
        <CheckBox android:id="@+id/cbOliwki"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:text="Oliwki (+3 zł)" />
        <CheckBox android:id="@+id/cbJalapeno"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:text="Jalapeño (+2 zł)" />
        <CheckBox android:id="@+id/cbAnanas"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:text="Ananas (+4 zł)" />

        <Button
            android:id="@+id/btnBezDodatkow"
            style="@style/Widget.Material3.Button.TextButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Bez dodatków" />

        <TextView
            android:id="@+id/tvDodatki"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/tvCena"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="24sp"
            android:textStyle="bold" />

        <CheckBox
            android:id="@+id/cbRegulamin"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="Akceptuję regulamin" />

        <Button
            android:id="@+id/btnZamow"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:enabled="false"
            android:text="Zamów" />

        <TextView
            android:id="@+id/tvPotwierdzenie"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:textColor="#2E7D32" />
    </LinearLayout>
</ScrollView>
MainActivity.kt — pełny plik
package pl.schoolit.pizza

import android.os.Bundle
import android.widget.Button
import android.widget.CheckBox
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 cbSer = findViewById<CheckBox>(R.id.cbSer)
        val cbSzynka = findViewById<CheckBox>(R.id.cbSzynka)
        val cbPieczarki = findViewById<CheckBox>(R.id.cbPieczarki)
        val cbOliwki = findViewById<CheckBox>(R.id.cbOliwki)
        val cbJalapeno = findViewById<CheckBox>(R.id.cbJalapeno)
        val cbAnanas = findViewById<CheckBox>(R.id.cbAnanas)
        val cbRegulamin = findViewById<CheckBox>(R.id.cbRegulamin)
        val btnBezDodatkow = findViewById<Button>(R.id.btnBezDodatkow)
        val btnZamow = findViewById<Button>(R.id.btnZamow)
        val tvDodatki = findViewById<TextView>(R.id.tvDodatki)
        val tvCena = findViewById<TextView>(R.id.tvCena)
        val tvPotwierdzenie = findViewById<TextView>(R.id.tvPotwierdzenie)

        val cenaBazowa = 24

        // liczy cenę i listę dodatków na podstawie WSZYSTKICH pól
        fun przelicz() {
            var cena = cenaBazowa
            var ile = 0
            var lista = ""

            if (cbSer.isChecked) { cena += 4; ile++; lista += "ser, " }
            if (cbSzynka.isChecked) { cena += 5; ile++; lista += "szynka, " }
            if (cbPieczarki.isChecked) { cena += 3; ile++; lista += "pieczarki, " }
            if (cbOliwki.isChecked) { cena += 3; ile++; lista += "oliwki, " }
            if (cbJalapeno.isChecked) { cena += 2; ile++; lista += "jalapeño, " }
            if (cbAnanas.isChecked) { cena += 4; ile++; lista += "ananas, " }

            if (ile >= 5) {
                cena -= 3
            }

            if (ile == 0) {
                tvDodatki.text = "Bez dodatków"
            } else {
                tvDodatki.text = lista.removeSuffix(", ")    // bez przecinka na końcu
            }
            tvCena.text = "Do zapłaty: $cena zł"
            tvPotwierdzenie.text = ""                        // zmiana unieważnia zamówienie
        }

        przelicz()

        // każde pole dodatku po zmianie przelicza całość
        cbSer.setOnCheckedChangeListener { _, _ -> przelicz() }
        cbSzynka.setOnCheckedChangeListener { _, _ -> przelicz() }
        cbPieczarki.setOnCheckedChangeListener { _, _ -> przelicz() }
        cbOliwki.setOnCheckedChangeListener { _, _ -> przelicz() }
        cbJalapeno.setOnCheckedChangeListener { _, _ -> przelicz() }
        cbAnanas.setOnCheckedChangeListener { _, _ -> przelicz() }

        // regulamin włącza i wyłącza przycisk
        cbRegulamin.setOnCheckedChangeListener { _, zaznaczony ->
            btnZamow.isEnabled = zaznaczony
        }

        btnBezDodatkow.setOnClickListener {
            cbSer.isChecked = false
            cbSzynka.isChecked = false
            cbPieczarki.isChecked = false
            cbOliwki.isChecked = false
            cbJalapeno.isChecked = false
            cbAnanas.isChecked = false
            // przelicz() wywoła się samo — zmiana z kodu też uruchamia listener
        }

        btnZamow.setOnClickListener {
            tvPotwierdzenie.text = "Przyjęto zamówienie. ${tvCena.text}"
        }
    }
}

Co dzieje się po kolei

  • przelicz() zaczyna od ceny bazowej i dla każdego zaznaczonego pola dolicza cenę, zwiększa licznik i dopisuje nazwę do listy. Średniki pozwalają zmieścić trzy krótkie instrukcje w jednej linii.
  • Na końcu removeSuffix(", ") usuwa ostatni przecinek — lista „ser, pieczarki, ” staje się „ser, pieczarki”.
  • Wszystkie pola dodatków mają ten sam listener: { _, _ -> przelicz() }. Nie interesuje nas, które pole się zmieniło — liczymy wszystko od nowa.
  • btnBezDodatkow tylko odznacza pola. Każde odznaczenie uruchamia listener, więc cena przelicza się sama.
  • btnZamow ma w XML enabled="false". Włącza go dopiero zaznaczenie regulaminu — wartość zaznaczony trafia wprost do isEnabled.

Sprawdź trzy rzeczy

  1. Zaznacz pięć dodatków. Cena wyniesie 24 + suma − 3 zł. Odznacz jeden — rabat zniknie.
  2. Zamów, a potem zmień dodatek. Potwierdzenie zniknie — zamówienie nie odpowiada już wyborowi.
  3. Kliknij w napis „Szynka”, a nie w kwadracik. Pole też się zaznaczy.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<CheckBox>kontrolkaPole wyboru z podpisem.
android:checked="true"atrybutZaznaczone na starcie.
cb.isCheckedBooleanOdczyt i zmiana stanu.
cb.toggle()metodaPrzełącza stan na przeciwny.
cb.setOnCheckedChangeListener { _, zaznaczony -> }metodaKod po każdej zmianie stanu.
Widget.Material3.Button.TextButtonstylPrzycisk wyglądający jak tekst.
napis.removeSuffix(", ")StringUsuwa podany fragment z końca, jeśli tam jest.
if (warunek) instrukcjaskrótif bez klamer dla jednej instrukcji.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
if (cbSer.isChecked == "true")Błąd kompilacji — isChecked to Boolean, nie napis. Wystarczy if (cbSer.isChecked).
cena liczy tylko ostatnio kliknięty dodatekKażdy listener dolicza do wspólnej sumy zamiast liczyć wszystko od nowa. Licz w jednej funkcji od ceny bazowej.
suma rośnie przy każdym kliknięciu, także przy odznaczaniuListener dodaje cenę bez sprawdzenia stanu. Sprawdzaj isChecked.
przycisk „Wyczyść” odznacza, ale cena się nie zmieniaUżyto setOnClickListener na polach — nie reaguje na zmiany z kodu.
zmienna cena poza funkcją, a przelicz() tylko dodajeCena rośnie bez końca. Zmienną liczoną od nowa deklaruj wewnątrz funkcji.
pola wyboru w RadioGroupRadioGroup obsługuje tylko RadioButton (lekcja 10). CheckBoksy wkładaj w zwykły układ.
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 1Zgody w formularzu★☆☆

Trzy pola: „Akceptuję regulamin” (wymagane), „Zgoda na newsletter”, „Mam ukończone 16 lat” (wymagane). Przycisk Załóż konto jest aktywny tylko wtedy, gdy oba wymagane pola są zaznaczone.

pokaż wskazówkę
  • Funkcja sprawdz(): btn.isEnabled = cb1.isChecked && cb3.isChecked, wywoływana z listenerów obu wymaganych pól.
ZAD 2Zaznacz wszystkie★☆☆

Lista pięciu zadań domowych jako pola wyboru i nad nimi pole „Zaznacz wszystkie”. Jego zaznaczenie zaznacza wszystkie zadania, odznaczenie — odznacza. Pod listą: „Zrobione: 3 z 5”.

pokaż wskazówkę
  • W listenerze „Zaznacz wszystkie” ustaw isChecked = zaznaczony każdemu z pięciu pól.
ZAD 3Ankieta zainteresowań★★☆

Osiem zainteresowań (sport, muzyka, gry, książki, film, programowanie, podróże, gotowanie). Użytkownik może wybrać najwyżej trzy: po zaznaczeniu trzeciego pozostałe pola się wyłączają, po odznaczeniu — znów włączają. Na dole „Wybrano: gry, programowanie (2/3)”.

pokaż wskazówkę
  • Trzymaj pola w tablicy val pola = arrayOf(cb1, …, cb8). Liczba zaznaczonych: pola.count { it.isChecked }. Wyłączanie w pętli: niezaznaczone pole dostaje isEnabled = ile < 3.
ZAD 4Kalkulator kosztów wycieczki★★☆

Pole tekstowe z liczbą uczestników i pola wyboru opcji: autokar (60 zł/os.), bilety do muzeum (25 zł/os.), obiad (35 zł/os.), przewodnik (300 zł za całą grupę), ubezpieczenie (8 zł/os.). Koszt całkowity i na osobę przeliczają się przy każdej zmianie pola wyboru i po kliknięciu Przelicz.

pokaż wskazówkę
  • W przelicz() odczytaj liczbę osób przez toIntOrNull() (lekcja 06); gdy jej brak — pokaż komunikat i zakończ funkcję przez return.
ZAD 5Lista kontrolna przed sprawdzianem★★☆

Sześć punktów do odhaczenia przed sprawdzianem z programowania. Pasek z napisem „Gotowość: 50%” zmienia kolor tła: czerwony poniżej 50%, pomarańczowy 50–99%, zielony przy 100%. Przy 100% pojawia się „Powodzenia!”.

pokaż wskazówkę
  • Procent: ile * 100 / 6. Kolor tła napisu zmienisz przez setBackgroundColor (lekcja 04).
ZAD 6Konfigurator laptopa★★★

Laptop bazowy 2800 zł. Opcje: dodatkowe 16 GB RAM (+400), dysk 1 TB zamiast 512 GB (+350), ekran OLED (+900), podświetlana klawiatura (+150), przedłużona gwarancja (+10% ceny wszystkich pozostałych elementów), torba (+120). Ekran OLED wymaga dodatkowego RAM — zaznaczenie OLED samo zaznacza RAM, a odznaczenie RAM odznacza OLED. Podsumowanie: lista wybranych opcji z cenami w osobnych liniach i suma.

pokaż wskazówkę
  • Gwarancję policz na końcu przelicz(), od sumy reszty. Zależność OLED ↔ RAM zapisz w dwóch listenerach: if (zaznaczony) cbRam.isChecked = true oraz if (!zaznaczony) cbOled.isChecked = false.
PODSUMOWANIE

Co trzeba zapamiętać

  • CheckBox służy do wyboru dowolnie wielu opcji; każde pole działa niezależnie.
  • Stan: isChecked — odczyt i zmiana; na starcie android:checked.
  • Na zmianę reaguj przez setOnCheckedChangeListener — działa także przy zmianach z kodu.
  • Wynik zależny od wielu pól licz w jednej funkcji, zawsze od początku.
  • Przycisk zależny od zgody: btn.isEnabled = zaznaczony.

Dokumentacja: Pola wyboru.

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