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

school-it.pl // android + kotlin // rozdział 6 // lekcja 21

Toast z własnym wyglądem i Snackbar

Zwykły Toast to zawsze szary dymek z tekstem. W tej lekcji zaprojektujesz własny wygląd komunikatu — z ikoną, tytułem i kolorowym tłem — i dowiesz się, dlaczego Google uznaje ten sposób za przestarzały. Poznasz też jego nowocześniejszego następcę: Snackbar, pasek u dołu ekranu z przyciskiem akcji, np. „Cofnij”. Oba połączysz w liście zadań na dziś.

Kotlin XML Toast.view LayoutInflater Snackbar setAction 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zaprojektujesz układ komunikatu w osobnym pliku XML.
  • Zamienisz plik XML na obiekt widoku metodą inflate.
  • Pokażesz Toast z własnym wyglądem i ustawisz jego położenie.
  • Wyjaśnisz ograniczenia własnych Toastów w nowych Androidach.
  • Pokażesz Snackbar z przyciskiem „Cofnij”.
TEORIA

Układ komunikatu

Własny Toast to zwykły plik układu w res/layout — taki jak activity_main.xml, tylko mały. Można w nim użyć kształtu z lekcji 17 jako tła, ikony, kilku napisów:

res/layout/toast_odznaka.xml — skrót
<LinearLayout …
    android:background="@drawable/tlo_toast"
    android:orientation="horizontal"
    android:padding="16dp">
    <ImageView … android:src="@drawable/ic_gwiazda" />
    <TextView android:id="@+id/tvTytul" … />
</LinearLayout>
12:30
zwykły Toast
Wszystko zrobione
własny Toast
Brawo!
Wszystkie zadania zrobione
ten sam komunikat — dwa wyglądy
TEORIA

Z XML do obiektu: inflate

Dla ekranu plik XML zamieniał na kontrolki setContentView. Dla małego układu, który nie jest całym ekranem, robi to LayoutInflater — „nadmuchiwacz”: bierze płaski opis z XML i „nadmuchuje” go do prawdziwych obiektów widoków:

wczytanie układu i znalezienie napisu w nim
val widok = layoutInflater.inflate(R.layout.toast_odznaka, null)
val tvTytul = widok.findViewById<TextView>(R.id.tvTytul)    // szukamy W widoku, nie na ekranie
tvTytul.text = "Brawo!"
FragmentZnaczenie
layoutInflater„Nadmuchiwacz” dostępny w każdej aktywności.
inflate(R.layout.…, null)Tworzy widoki z pliku. null — nie wkładaj ich jeszcze nigdzie na ekranie.
widok.findViewByIdSzuka kontrolki wewnątrz wczytanego układu. Zwykłe findViewById szukałoby na ekranie aktywności i nie znalazłoby jej.

Ten sam mechanizm wróci w lekcji 24 (własny wygląd okna dialogowego) i w lekcji 28 (własny wiersz listy).

TEORIA

Własny Toast w kodzie

Toast z własnym widokiem
val toast = Toast(this)                       // pusty Toast — bez makeText
toast.duration = Toast.LENGTH_LONG
@Suppress("DEPRECATION")
toast.view = widok                            // nasz układ zamiast szarego dymka
toast.setGravity(Gravity.CENTER, 0, 0)        // dla własnego widoku położenie działa
toast.show()

Zamiast makeText tworzymy pusty Toast konstruktorem Toast(this) i podajemy mu widok. W odróżnieniu od Toasta tekstowego, własnemu widokowi można ustawić położenie przez setGravity — tu na środku ekranu.

TEORIA

Dlaczego „przestarzały”?

toast.view jest oznaczony jako deprecated od Androida 11

Android Studio przekreśla toast.view — dlatego w kodzie jest @Suppress("DEPRECATION"). Co to oznacza w praktyce:

  • Gdy aplikacja jest na ekranie, własny Toast nadal się pokazuje.
  • Gdy aplikacja działa w tle (np. użytkownik przełączył się na inną), własny Toast z Androida 11+ nie pojawi się wcale. Zwykły Toast tekstowy — tak.
  • Google może w przyszłości całkiem wyłączyć tę możliwość.

Własny Toast warto znać — występuje w wielu materiałach i zadaniach — ale w nowych aplikacjach Google zaleca zamiast niego Snackbar.

TEORIA

Snackbar

Snackbar to pasek wysuwający się u dołu ekranu. Pochodzi z biblioteki Material, którą szablon projektu już zawiera. Od Toasta różni się tym, że jest częścią twojego ekranu i może mieć jeden przycisk akcji:

Snackbar z akcją Cofnij
Snackbar.make(glownyUklad, "Usunięto zadanie", Snackbar.LENGTH_LONG)
    .setAction("Cofnij") {
        przywrocZadanie()                     // wykona się po kliknięciu „Cofnij”
    }
    .show()
FragmentZnaczenie
Snackbar.make(widok, tekst, czas)Tworzy pasek. Pierwszy parametr to dowolny widok na ekranie (zwykle główny układ) — Snackbar musi wiedzieć, na którym ekranie się pokazać.
Snackbar.LENGTH_SHORT / LONGOk. 1,5 s / 2,75 s.
Snackbar.LENGTH_INDEFINITEZostaje, aż użytkownik kliknie akcję albo pokażesz inny Snackbar.
.setAction("Cofnij") { … }Przycisk akcji po prawej stronie paska.
.show()Pokazuje pasek.

Snackbary nie ustawiają się w długą kolejkę — nowy zastępuje poprzedni. Klasa wymaga importu com.google.android.material.snackbar.Snackbar.

TEORIA

Toast czy Snackbar?

ToastSnackbar
gdzienad wszystkimi aplikacjamiwewnątrz ekranu twojej aplikacji
przycisk akcjinietak, jeden
własny wyglądprzestarzały (toast.view)kolory przez metody, np. setBackgroundTint
użytkownik może zamknąćnietak, przesunięciem w bok
typowe użycie„Zapisano”, „Skopiowano”„Usunięto · Cofnij”, „Brak internetu · Ponów”
PRZYKŁAD

Zadania na dziś

Cztery zadania jako pola wyboru, przy każdym ikona kosza. Usunięcie chowa wiersz i pokazuje Snackbar z akcją Cofnij, która przywraca zadanie. Licznik na górze pokazuje postęp. Gdy wszystkie pozostałe zadania są zrobione, pojawia się własny Toast-odznaka na środku ekranu.

16:10
Zadania na dziś
Zrobione: 1 z 3
Zadanie z matematyki
Trening piłki
Powtórka z Kotlina
Usunięto: Wynieść śmieci COFNIJ
po usunięciu zadania — Snackbar z akcją
→zaznaczenie
ostatnich
zadań
16:25
Zadania na dziś
Zrobione: 3 z 3
Zadanie z matematyki
Trening piłki
Powtórka z Kotlina
Brawo!
Zrobione wszystkie zadania (3)
własny Toast na środku ekranu

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa ZadaniaNaDzis, Kotlin, API 24.
  2. W res/drawable utwórz ikony ic_usun (Vector Asset → Clip art → delete) i ic_gwiazda (star) oraz kształt tlo_toast.xml.
  3. W res/layout utwórz toast_odznaka.xml (prawy przycisk na layout → New → Layout Resource File).
  4. Podmień activity_main.xml i MainActivity.kt.
res/
├── drawable/
│   ├── ic_usun.xmlVector Asset: delete
│   ├── ic_gwiazda.xmlVector Asset: star
│   └── tlo_toast.xmlkształt tła odznaki
└── layout/
    ├── activity_main.xml
    └── toast_odznaka.xmlwygląd własnego Toasta
res/drawable/tlo_toast.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#1B5E20" />
    <corners android:radius="20dp" />
</shape>
res/layout/toast_odznaka.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/tlo_toast"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="16dp">

    <ImageView
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:contentDescription="@null"
        android:src="@drawable/ic_gwiazda"
        android:tint="#FFD54F" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tvTytul"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:textSize="18sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/tvOpis"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#C8E6C9"
            android:textSize="14sp" />
    </LinearLayout>
</LinearLayout>
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:layout_margin="16dp"
    android:fitsSystemWindows="true">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Zadania na dziś"
        android:textSize="24sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tvPostep"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:textSize="16sp" />

    <!-- cztery wiersze: pole wyboru (waga 1) + kosz -->
    <LinearLayout android:id="@+id/wiersz0" android:layout_width="match_parent"
        android:layout_height="wrap_content" android:gravity="center_vertical">
        <CheckBox android:id="@+id/cb0" android:layout_width="0dp" android:layout_height="wrap_content"
            android:layout_weight="1" android:text="Wynieść śmieci" />
        <ImageButton android:id="@+id/ib0" android:layout_width="48dp" android:layout_height="48dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Usuń zadanie" android:src="@drawable/ic_usun" />
    </LinearLayout>

    <LinearLayout android:id="@+id/wiersz1" android:layout_width="match_parent"
        android:layout_height="wrap_content" android:gravity="center_vertical">
        <CheckBox android:id="@+id/cb1" android:layout_width="0dp" android:layout_height="wrap_content"
            android:layout_weight="1" android:text="Zadanie z matematyki" />
        <ImageButton android:id="@+id/ib1" android:layout_width="48dp" android:layout_height="48dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Usuń zadanie" android:src="@drawable/ic_usun" />
    </LinearLayout>

    <LinearLayout android:id="@+id/wiersz2" android:layout_width="match_parent"
        android:layout_height="wrap_content" android:gravity="center_vertical">
        <CheckBox android:id="@+id/cb2" android:layout_width="0dp" android:layout_height="wrap_content"
            android:layout_weight="1" android:text="Trening piłki" />
        <ImageButton android:id="@+id/ib2" android:layout_width="48dp" android:layout_height="48dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Usuń zadanie" android:src="@drawable/ic_usun" />
    </LinearLayout>

    <LinearLayout android:id="@+id/wiersz3" android:layout_width="match_parent"
        android:layout_height="wrap_content" android:gravity="center_vertical">
        <CheckBox android:id="@+id/cb3" android:layout_width="0dp" android:layout_height="wrap_content"
            android:layout_weight="1" android:text="Powtórka z Kotlina" />
        <ImageButton android:id="@+id/ib3" android:layout_width="48dp" android:layout_height="48dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Usuń zadanie" android:src="@drawable/ic_usun" />
    </LinearLayout>

</LinearLayout>
MainActivity.kt — pełny plik
package pl.schoolit.zadanianadzis

import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.widget.CheckBox
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar

class MainActivity : AppCompatActivity() {

    // ---------- kontrolki ----------
    private lateinit var glownyUklad: View
    private lateinit var tvPostep: TextView
    private lateinit var wiersze: Array<LinearLayout>
    private lateinit var pola: Array<CheckBox>
    private lateinit var kosze: Array<ImageButton>

    // ---------- stan ----------
    private var gratulacjePokazane = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initUI()
        ustawZdarzenia()
        pokazPostep()
    }

    private fun initUI() {
        glownyUklad = findViewById(R.id.main)
        tvPostep = findViewById(R.id.tvPostep)
        wiersze = arrayOf(findViewById(R.id.wiersz0), findViewById(R.id.wiersz1),
                          findViewById(R.id.wiersz2), findViewById(R.id.wiersz3))
        pola = arrayOf(findViewById(R.id.cb0), findViewById(R.id.cb1),
                       findViewById(R.id.cb2), findViewById(R.id.cb3))
        kosze = arrayOf(findViewById(R.id.ib0), findViewById(R.id.ib1),
                        findViewById(R.id.ib2), findViewById(R.id.ib3))
    }

    private fun ustawZdarzenia() {
        for (i in pola.indices) {
            pola[i].setOnCheckedChangeListener { _, _ -> pokazPostep() }
            kosze[i].setOnClickListener { usunZadanie(i) }
        }
    }

    // ---------- czynności ----------
    private fun usunZadanie(nr: Int) {
        wiersze[nr].isVisible = false
        pokazPostep()

        Snackbar.make(glownyUklad, "Usunięto: ${pola[nr].text}", Snackbar.LENGTH_LONG)
            .setAction("Cofnij") {
                wiersze[nr].isVisible = true
                pokazPostep()
            }
            .show()
    }

    private fun pokazPostep() {
        // liczymy tylko zadania, które są widoczne (nieusunięte)
        var wszystkie = 0
        var zrobione = 0
        for (i in pola.indices) {
            if (wiersze[i].isVisible) {
                wszystkie++
                if (pola[i].isChecked) zrobione++
            }
        }
        tvPostep.text = "Zrobione: $zrobione z $wszystkie"

        val komplet = wszystkie > 0 && zrobione == wszystkie
        if (komplet && !gratulacjePokazane) {
            pokazOdznake("Brawo!", "Zrobione wszystkie zadania ($wszystkie)")
        }
        gratulacjePokazane = komplet       // pokaż ponownie dopiero po „rozbiciu” kompletu
    }

    // ---------- własny Toast ----------
    private fun pokazOdznake(tytul: String, opis: String) {
        val widok = layoutInflater.inflate(R.layout.toast_odznaka, null)
        widok.findViewById<TextView>(R.id.tvTytul).text = tytul
        widok.findViewById<TextView>(R.id.tvOpis).text = opis

        val toast = Toast(this)
        toast.duration = Toast.LENGTH_LONG
        @Suppress("DEPRECATION")
        toast.view = widok
        toast.setGravity(Gravity.CENTER, 0, 0)
        toast.show()
    }
}

Co dzieje się po kolei

  • Cztery wiersze, pola i kosze są w trzech tablicach — element nr 2 w każdej tablicy dotyczy tego samego zadania. W pętli for (i in pola.indices) każdy kosz dostaje listener z własnym numerem i.
  • Usunięcie nie kasuje zadania, tylko chowa wiersz (isVisible = false). Dzięki temu „Cofnij” może go po prostu znowu pokazać.
  • Snackbar dostaje główny układ ekranu (R.id.main) — to wystarczy, by wiedział, gdzie się pojawić. Kod w setAction wykona się tylko po kliknięciu „Cofnij”.
  • pokazPostep() liczy tylko widoczne zadania. Gdy wszystkie widoczne są zaznaczone, pokazuje odznakę.
  • Pole gratulacjePokazane pilnuje, żeby odznaka nie wyskakiwała przy każdym kliknięciu, gdy komplet już był. Znów pojawi się dopiero, gdy ktoś odznaczy zadanie i zaznaczy je ponownie.
  • pokazOdznake „nadmuchuje” układ, wpisuje teksty do jego napisów i podaje go jako widok Toasta.

Sprawdź trzy rzeczy

  1. Usuń dwa zadania jedno po drugim. Drugi Snackbar zastąpi pierwszy — „Cofnij” przywróci tylko ostatnio usunięte.
  2. Zaznacz wszystkie zadania — pojawi się odznaka. Kliknij jeszcze raz w zaznaczone zadanie, żeby je odznaczyć, i zaznacz ponownie — odznaka wróci. Samo klikanie w inne miejsca jej nie powtarza.
  3. Przesuń Snackbar palcem w bok — zniknie wcześniej.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
layoutInflater.inflate(R.layout.…, null)ViewTworzy widoki z pliku układu.
widok.findViewById<T>(…)metodaSzuka kontrolki w podanym widoku.
Toast(this)ToastPusty Toast do własnego widoku.
toast.view = widokwłaściwośćWłasny wygląd (przestarzałe od API 30).
toast.durationIntCzas: LENGTH_SHORT / LENGTH_LONG.
toast.setGravity(Gravity.CENTER, x, y)metodaPołożenie własnego Toasta.
@Suppress("DEPRECATION")adnotacjaWycisza ostrzeżenie o przestarzałym elemencie.
Snackbar.make(widok, tekst, czas)SnackbarPasek komunikatu na ekranie.
.setAction("…") { }metodaPrzycisk akcji w pasku.
Snackbar.LENGTH_INDEFINITEstałaPasek do czasu kliknięcia akcji.
Array<CheckBox> jako poletablicaWiele kontrolek pod jednym polem.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
findViewById<TextView>(R.id.tvTytul) zamiast widok.findViewByIdZwraca null — napis jest w układzie Toasta, nie na ekranie. Awaria NullPointerException.
własny Toast nie pojawia się, gdy aplikacja jest w tleOgraniczenie Androida 11+. Własne Toasty działają tylko na pierwszym planie.
Unresolved reference: SnackbarBrak importu com.google.android.material.snackbar.Snackbar.
Snackbar pod paskiem nawigacji lub poza ekranemJako pierwszy parametr podano widok, który jest ukryty. Podawaj główny układ ekranu.
„Cofnij” przywraca złe zadanieNumer zadania nie został zapamiętany — lambda w setAction musi korzystać z parametru metody (nr), a nie z pola zmienianego później.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Empty Views Activity, w układzie z lekcji 19. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Trzy rodzaje komunikatów★☆☆

Własne Toasty w trzech wariantach: sukces (zielony, ikona check circle), ostrzeżenie (pomarańczowy, warning), błąd (czerwony, error). Jeden plik układu, a kolor tła i ikona ustawiane z kodu w metodzie pokazKomunikat(rodzaj: String, tekst: String).

pokaż wskazówkę
  • Kolor tła kształtu z kodu: (widok.background.mutate() as GradientDrawable).setColor(…) — lekcja 17. Ikona: widok.findViewById<ImageView>(…).setImageResource(…).
ZAD 2Kolorowy Snackbar★☆☆

Przycisk Wyślij pokazuje Snackbar „Wysłano wiadomość” z fioletowym tłem, białym tekstem i żółtą akcją Pokaż, która wypisuje treść wiadomości w TextView.

pokaż wskazówkę
  • Metody Snackbara: setBackgroundTint(kolor), setTextColor(kolor), setActionTextColor(kolor) — łańcuchowo przed show().
ZAD 3Kosz na notatki★★☆

Pole tekstowe i przycisk Wyczyść. Wyczyszczenie zapamiętuje tekst w polu klasy i pokazuje Snackbar „Wyczyszczono” z akcją Cofnij, która przywraca tekst. Gdy pole było puste, przycisk nic nie robi i jest wyłączony.

pokaż wskazówkę
  • Aktywność przycisku zależna od tekstu: et.addTextChangedListener { btn.isEnabled = !it.isNullOrEmpty() } (import androidx.core.widget.addTextChangedListener).
ZAD 4Odznaki za naukę★★☆

Przycisk Zrobiłem lekcję zwiększa licznik. Przy 1, 5, 10 i 20 lekcjach pojawia się własny Toast-odznaka z inną nazwą (Nowicjusz, Pilny uczeń, Wytrwały, Mistrz) i inną ikoną.

pokaż wskazówkę
  • Progi, nazwy i ikony w trzech równoległych tablicach. Sprawdzaj progi.indexOf(licznik) — wynik −1 oznacza „nie ma odznaki”.
ZAD 5Snackbar bez końca★★☆

Symulacja braku internetu: przełącznik „Tryb samolotowy”. Włączenie pokazuje Snackbar LENGTH_INDEFINITE „Brak połączenia” z akcją Ponów. Ponów przy nadal włączonym trybie pokazuje Snackbar jeszcze raz; po wyłączeniu przełącznika Snackbar znika sam.

pokaż wskazówkę
  • Zapamiętaj Snackbar w polu private var pasek: Snackbar? = null; ukrywanie: pasek?.dismiss().
ZAD 6Kosz z historią★★★

Rozbuduj przykład: usuwanie kilku zadań pod rząd, a przycisk Przywróć wszystko (aktywny tylko, gdy coś usunięto) przywraca wszystkie ukryte. Snackbar pokazuje liczbę usuniętych: „Usunięto 3 zadania · Cofnij ostatnie”. Gdy na liście nie zostało nic, pokaż napis „Brak zadań na dziś 🎉” zamiast licznika.

pokaż wskazówkę
  • Kolejność usuwania trzymaj na liście private val usuniete = mutableListOf<Int>(); „Cofnij ostatnie” zdejmuje ostatni element: usuniete.removeAt(usuniete.lastIndex).
PODSUMOWANIE

Co trzeba zapamiętać

  • Własny wygląd komunikatu projektujesz jako zwykły plik w res/layout.
  • layoutInflater.inflate(…, null) zamienia XML na widok; kontrolki w nim szukasz przez widok.findViewById.
  • Własny Toast: Toast(this) + toast.view = widok — przestarzałe, działa tylko na pierwszym planie.
  • Nowocześniej: Snackbar.make(glownyUklad, tekst, czas).setAction("Cofnij") { … }.show().
  • Snackbar wybieraj, gdy komunikat potrzebuje akcji; Toast — do prostego potwierdzenia.

Dokumentacja: Toast — w tym ograniczenia własnych widoków, Snackbar, Material 3 — Snackbar.

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