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ś.
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”.
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:
<LinearLayout …
android:background="@drawable/tlo_toast"
android:orientation="horizontal"
android:padding="16dp">
<ImageView … android:src="@drawable/ic_gwiazda" />
<TextView android:id="@+id/tvTytul" … />
</LinearLayout>
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:
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!"
| Fragment | Znaczenie |
|---|---|
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.findViewById | Szuka 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).
Własny Toast w kodzie
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.
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.
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.make(glownyUklad, "Usunięto zadanie", Snackbar.LENGTH_LONG)
.setAction("Cofnij") {
przywrocZadanie() // wykona się po kliknięciu „Cofnij”
}
.show()
| Fragment | Znaczenie |
|---|---|
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 / LONG | Ok. 1,5 s / 2,75 s. |
Snackbar.LENGTH_INDEFINITE | Zostaje, 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.
Toast czy Snackbar?
| Toast | Snackbar | |
|---|---|---|
| gdzie | nad wszystkimi aplikacjami | wewnątrz ekranu twojej aplikacji |
| przycisk akcji | nie | tak, jeden |
| własny wygląd | przestarzały (toast.view) | kolory przez metody, np. setBackgroundTint |
| użytkownik może zamknąć | nie | tak, przesunięciem w bok |
| typowe użycie | „Zapisano”, „Skopiowano” | „Usunięto · Cofnij”, „Brak internetu · Ponów” |
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.
ostatnich
zadań
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
ZadaniaNaDzis, Kotlin, API 24. - W
res/drawableutwórz ikonyic_usun(Vector Asset → Clip art → delete) iic_gwiazda(star) oraz kształttlo_toast.xml. - W
res/layoututwórztoast_odznaka.xml(prawy przycisk nalayout→ New → Layout Resource File). - Podmień
activity_main.xmliMainActivity.kt.
├── 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
<?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>
<?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>
<?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>
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 numeremi. - 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 wsetActionwykona się tylko po kliknięciu „Cofnij”. pokazPostep()liczy tylko widoczne zadania. Gdy wszystkie widoczne są zaznaczone, pokazuje odznakę.- Pole
gratulacjePokazanepilnuje, ż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
- Usuń dwa zadania jedno po drugim. Drugi Snackbar zastąpi pierwszy — „Cofnij” przywróci tylko ostatnio usunięte.
- 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.
- Przesuń Snackbar palcem w bok — zniknie wcześniej.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
layoutInflater.inflate(R.layout.…, null) | View | Tworzy widoki z pliku układu. |
widok.findViewById<T>(…) | metoda | Szuka kontrolki w podanym widoku. |
Toast(this) | Toast | Pusty Toast do własnego widoku. |
toast.view = widok | właściwość | Własny wygląd (przestarzałe od API 30). |
toast.duration | Int | Czas: LENGTH_SHORT / LENGTH_LONG. |
toast.setGravity(Gravity.CENTER, x, y) | metoda | Położenie własnego Toasta. |
@Suppress("DEPRECATION") | adnotacja | Wycisza ostrzeżenie o przestarzałym elemencie. |
Snackbar.make(widok, tekst, czas) | Snackbar | Pasek komunikatu na ekranie. |
.setAction("…") { } | metoda | Przycisk akcji w pasku. |
Snackbar.LENGTH_INDEFINITE | stała | Pasek do czasu kliknięcia akcji. |
Array<CheckBox> jako pole | tablica | Wiele kontrolek pod jednym polem. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
findViewById<TextView>(R.id.tvTytul) zamiast widok.findViewById | Zwraca null — napis jest w układzie Toasta, nie na ekranie. Awaria NullPointerException. |
| własny Toast nie pojawia się, gdy aplikacja jest w tle | Ograniczenie Androida 11+. Własne Toasty działają tylko na pierwszym planie. |
| Unresolved reference: Snackbar | Brak importu com.google.android.material.snackbar.Snackbar. |
| Snackbar pod paskiem nawigacji lub poza ekranem | Jako pierwszy parametr podano widok, który jest ukryty. Podawaj główny układ ekranu. |
| „Cofnij” przywraca złe zadanie | Numer zadania nie został zapamiętany — lambda w setAction musi korzystać z parametru metody (nr), a nie z pola zmienianego później. |
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.
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(…).
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 przedshow().
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() }(importandroidx.core.widget.addTextChangedListener).
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”.
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().
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).
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 przezwidok.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.