school-it.pl // android + kotlin // rozdział 6 // lekcja 24
AlertDialog z własnym wyglądem
Wbudowane okna mają tytuł, treść, listę i przyciski. A gdy w oknie ma być formularz — login i hasło, ocena w gwiazdkach z komentarzem, wybór koloru suwakami? Wtedy projektujesz własny układ w XML i wkładasz go do okna metodą setView. W tej lekcji zbudujesz okno logowania, które sprawdza dane i nie zamyka się, dopóki nie będą poprawne.
Czego się dziś nauczysz
- Zaprojektujesz układ okna dialogowego w osobnym pliku XML.
- Włożysz go do okna metodą
setView. - Odczytasz dane z pól znajdujących się w oknie.
- Sprawisz, że przycisk okna sprawdza dane i zamyka okno tylko wtedy, gdy są poprawne.
- Zamkniesz okno z kodu metodą
dismiss().
Układ okna
Zawartość okna to zwykły plik w res/layout — jak układ własnego Toasta w lekcji 21. Tytuł i przyciski nadal daje AlertDialog.Builder, a twój układ wypełnia środek okna. Dlatego w pliku nie umieszczasz tytułu ani przycisków OK / Anuluj.
fioletowa — twój układ (setView)
setView
val widok = layoutInflater.inflate(R.layout.dialog_logowanie, null)
AlertDialog.Builder(this)
.setTitle("Logowanie")
.setView(widok) // twój układ w środku okna
.setPositiveButton("Zaloguj") { _, _ -> … }
.setNegativeButton("Anuluj", null)
.show()
inflate zamienia plik XML na widok — dokładnie jak w lekcji 21. Każde otwarcie okna to nowe wywołanie inflate, więc pola w oknie są za każdym razem puste.
Kontrolki w oknie
Kontrolki z układu okna nie są częścią ekranu aktywności. Zwykłe findViewById ich nie znajdzie — szukasz ich w widoku okna:
val etLogin = widok.findViewById<EditText>(R.id.etLogin)
val etHaslo = widok.findViewById<EditText>(R.id.etHaslo)
To zmienne lokalne metody, która otwiera okno — nie pola klasy. Okno żyje krótko, a przy każdym otwarciu powstają nowe kontrolki.
Okno, które się nie zamyka
Przyciski ustawione przez setPositiveButton zawsze zamykają okno po kliknięciu — także wtedy, gdy hasło jest błędne. Użytkownik musiałby otwierać okno od nowa i wpisywać wszystko jeszcze raz. Rozwiązanie w trzech krokach:
// 1. przycisk z null — Builder go utworzy, ale bez naszej akcji
val okno = AlertDialog.Builder(this)
.setTitle("Logowanie")
.setView(widok)
.setPositiveButton("Zaloguj", null)
.setNegativeButton("Anuluj", null)
.show() // show() zwraca gotowe okno
// 2. po show() przyciski już istnieją — podmieniamy listener przycisku „Zaloguj”
okno.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
if (daneSaPoprawne()) {
okno.dismiss() // 3. zamykamy SAMI — tylko przy poprawnych danych
} else {
etHaslo.error = "Błędne hasło" // okno zostaje otwarte
}
}
| Element | Znaczenie |
|---|---|
val okno = ….show() | show() zwraca utworzone okno — zapamiętujemy je w zmiennej. |
okno.getButton(AlertDialog.BUTTON_POSITIVE) | Przycisk pozytywny okna. Są też BUTTON_NEGATIVE i BUTTON_NEUTRAL. |
.setOnClickListener { … } | Nasz listener zastępuje domyślny — ten, który zamykał okno. |
okno.dismiss() | Zamyka okno z kodu. |
getButton dopiero po show()
Przed show() okno nie ma jeszcze przycisków i getButton zwróci null — aplikacja się zamknie. Kolejność: show(), potem getButton.
Logowanie do dziennika
Ekran główny pokazuje stan: „Niezalogowany” i przycisk Zaloguj. Przycisk otwiera okno z polami login i hasło oraz polem wyboru „Pokaż hasło”. Błędne dane pokazują komunikat w oknie i okno zostaje otwarte; po trzech błędach okno się zamyka i pojawia się Toast. Poprawne dane (login uczen, hasło android1) zamykają okno, a ekran wita użytkownika.
hasło
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Dziennik, Kotlin, API 24. - W
res/layoututwórz plikdialog_logowanie.xml. - Podmień
activity_main.xmliMainActivity.kt.
<?xml version="1.0" encoding="utf-8"?>
<!-- tylko środek okna: bez tytułu i bez przycisków OK / Anuluj -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:paddingEnd="24dp">
<EditText
android:id="@+id/etLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Login"
android:inputType="text" />
<EditText
android:id="@+id/etHaslo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Hasło"
android:inputType="textPassword" />
<CheckBox
android:id="@+id/cbPokaz"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pokaż hasło" />
<TextView
android:id="@+id/tvBlad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#B3261E"
android:textSize="14sp" />
</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="Dziennik szkolny"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvStan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="20sp" />
<TextView
android:id="@+id/tvGodzina"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp" />
<Button
android:id="@+id/btnLogowanie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp" />
</LinearLayout>
package pl.schoolit.dziennik
import android.graphics.Color
import android.os.Bundle
import android.text.InputType
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import java.util.Calendar
class MainActivity : AppCompatActivity() {
// ---------- kontrolki ekranu ----------
private lateinit var tvStan: TextView
private lateinit var tvGodzina: TextView
private lateinit var btnLogowanie: Button
// ---------- stan ----------
private var zalogowany: String? = null // null = nikt nie jest zalogowany
private val poprawnyLogin = "uczen"
private val poprawneHaslo = "android1"
private val limitProb = 3
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
ustawZdarzenia()
pokazStan()
}
private fun initUI() {
tvStan = findViewById(R.id.tvStan)
tvGodzina = findViewById(R.id.tvGodzina)
btnLogowanie = findViewById(R.id.btnLogowanie)
}
private fun ustawZdarzenia() {
btnLogowanie.setOnClickListener {
if (zalogowany == null) {
pokazOknoLogowania()
} else {
zalogowany = null
pokazStan()
}
}
}
// ---------- okno z własnym układem ----------
private fun pokazOknoLogowania() {
// układ okna i jego kontrolki — zmienne LOKALNE, nowe przy każdym otwarciu
val widok = layoutInflater.inflate(R.layout.dialog_logowanie, null)
val etLogin = widok.findViewById<EditText>(R.id.etLogin)
val etHaslo = widok.findViewById<EditText>(R.id.etHaslo)
val cbPokaz = widok.findViewById<CheckBox>(R.id.cbPokaz)
val tvBlad = widok.findViewById<TextView>(R.id.tvBlad)
var bledy = 0
// pole wyboru w oknie działa jak każde inne
cbPokaz.setOnCheckedChangeListener { _, pokaz ->
etHaslo.inputType = if (pokaz) {
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
} else {
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
etHaslo.setSelection(etHaslo.text.length) // kursor na koniec
}
// 1. przycisk „Zaloguj” z null — sami zdecydujemy o zamknięciu
val okno = AlertDialog.Builder(this)
.setTitle("Logowanie")
.setView(widok)
.setPositiveButton("Zaloguj", null)
.setNegativeButton("Anuluj", null)
.setCancelable(false)
.show()
// 2. po show() podmieniamy listener przycisku „Zaloguj”
okno.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val login = etLogin.text.toString().trim()
val haslo = etHaslo.text.toString()
if (login.isEmpty()) {
etLogin.error = "Podaj login"
return@setOnClickListener
}
if (haslo.isEmpty()) {
etHaslo.error = "Podaj hasło"
return@setOnClickListener
}
if (login == poprawnyLogin && haslo == poprawneHaslo) {
zalogowany = login
pokazStan()
okno.dismiss() // 3. zamykamy tylko przy sukcesie
return@setOnClickListener
}
bledy++
if (bledy >= limitProb) {
okno.dismiss()
Toast.makeText(this, "Za dużo błędnych prób", Toast.LENGTH_LONG).show()
} else {
tvBlad.text = "Błędny login lub hasło. Pozostało prób: ${limitProb - bledy}"
etHaslo.setText("")
etHaslo.requestFocus()
}
}
}
// ---------- stan → ekran ----------
private fun pokazStan() {
val uzytkownik = zalogowany
if (uzytkownik == null) {
tvStan.text = "Niezalogowany"
tvStan.setTextColor(Color.GRAY)
tvGodzina.text = ""
btnLogowanie.text = "Zaloguj"
} else {
val teraz = Calendar.getInstance()
tvStan.text = "Witaj, $uzytkownik!"
tvStan.setTextColor(Color.parseColor("#2E7D32"))
tvGodzina.text = String.format("zalogowano o %d:%02d",
teraz.get(Calendar.HOUR_OF_DAY), teraz.get(Calendar.MINUTE))
btnLogowanie.text = "Wyloguj"
}
}
}
Co dzieje się po kolei
dialog_logowanie.xmlzawiera tylko środek okna — tytuł i przyciski daje Builder. Boczny padding 24dp wyrównuje pola z tytułem okna.pokazOknoLogowania()przy każdym wywołaniu tworzy nowy widok, więc pola zawsze startują puste. Kontrolki okna są zmiennymi lokalnymi tej metody.- Pole wyboru „Pokaż hasło” ma własny listener — kontrolki w oknie działają tak samo jak na ekranie.
- „Zaloguj” jest utworzony z
null, a poshow()dostaje nasz listener. Ten listener sam decyduje: przy błędzie pokazuje komunikat i zostawia okno, przy sukcesie wywołujeokno.dismiss(). - Licznik
bledyjest lokalny dla okna — każde nowe otwarcie daje trzy nowe próby. setCancelable(false): dotknięcie obok nie zgubi wpisanego loginu. „Anuluj” nadal działa — ma domyślne zachowanie.
To tylko ćwiczenie
Hasło wpisane w kod aplikacji każdy może odczytać z pliku APK. Prawdziwe logowanie sprawdza dane na serwerze. Tu ćwiczymy wyłącznie budowę okna.
Sprawdź trzy rzeczy
- Wpisz złe hasło dwa razy — okno zostaje, licznik prób maleje. Za trzecim razem okno się zamyka.
- Zakomentuj blok
okno.getButton(…). Teraz „Zaloguj” zawsze zamyka okno i nic się nie sprawdza. - Przenieś blok
getButtonprzed.show()(zmieniającshow()nacreate()). Aplikacja zamknie się przy otwieraniu okna — przycisków jeszcze nie ma.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
layoutInflater.inflate(R.layout.dialog_…, null) | View | Widok zawartości okna. |
.setView(widok) | metoda | Własny układ w środku okna. |
widok.findViewById<T>(…) | metoda | Kontrolka z układu okna. |
val okno = builder.show() | AlertDialog | Pokazuje okno i zwraca je. |
builder.create() | AlertDialog | Tworzy okno bez pokazywania. |
okno.getButton(AlertDialog.BUTTON_POSITIVE) | Button | Przycisk okna (po show()). |
okno.dismiss() | metoda | Zamyka okno z kodu. |
InputType.TYPE_TEXT_VARIATION_PASSWORD / …VISIBLE_PASSWORD | stałe | Hasło ukryte / widoczne. |
et.setSelection(pozycja) | metoda | Ustawia kursor w polu. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
findViewById<EditText>(R.id.etLogin) bez widok. | Zwraca null — pole jest w oknie, nie na ekranie. Awaria NullPointerException. |
| okno zamyka się mimo złego hasła | Logika jest w lambdzie setPositiveButton. Użyj null i getButton po show(). |
getButton przed show() | Zwraca null — awaria. Najpierw show(). |
| okno nie zamyka się po poprawnych danych | Brak okno.dismiss() w naszym listenerze. |
| przy drugim otwarciu w polach jest stary tekst | Widok utworzony raz i zapamiętany w polu klasy. Twórz go w metodzie otwierającej okno. |
| The specified child already has a parent | Ten sam widok podany do drugiego okna. Każde okno potrzebuje nowego inflate. |
| w układzie okna są przyciski OK/Anuluj i dodatkowo Builder dodaje swoje | Podwójne przyciski. Przyciski zostaw Builderowi albo usuń setPositiveButton i zamykaj okno własnymi przyciskami z układu. |
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.
Okno z RatingBar (5 gwiazdek) i polem „Komentarz (opcjonalnie)”. Po kliknięciu Wyślij ekran pokazuje ocenę i komentarz. Okno może się zamknąć zawsze (bez sprawdzania).
pokaż wskazówkę
- Tu wystarczy zwykła lambda
setPositiveButton— odczytaj w niejrb.ratingi tekst pola z widoku okna.
Przycisk Dodaj ucznia otwiera okno z polami imię, nazwisko i spinnerem klasy (1TP–5TP). Po zatwierdzeniu uczeń dopisuje się do listy w TextView: „Nowak Anna (3TP)”. Puste imię lub nazwisko — błąd przy polu i okno zostaje.
pokaż wskazówkę
- Spinner w oknie wypełnij przez
android:entriesw XML okna. WzórgetButton+dismiss()jak w przykładzie.
Okno z trzema suwakami RGB (lekcja 14) i prostokątem podglądu, który zmienia się w trakcie przesuwania. Po OK wybrany kolor staje się tłem ekranu. Okno startuje z obecnym kolorem tła.
pokaż wskazówkę
- Obecny kolor trzymaj w polu jako trzy liczby. Ustaw nimi
progresssuwaków przedshow().
Okno z polami: obecne hasło, nowe hasło, powtórz nowe. Reguły: obecne musi się zgadzać, nowe ma co najmniej 8 znaków i cyfrę, oba nowe są identyczne, nowe różni się od obecnego. Każdy błąd przy swoim polu, okno zostaje otwarte. Po sukcesie Snackbar „Hasło zmienione”.
pokaż wskazówkę
- Sprawdzaj po kolei i przerywaj przez
return@setOnClickListenerprzy pierwszym błędzie — jak w lekcji 06.
Okno „Twój wynik” z dużą liczbą punktów, gwiazdkami tylko do odczytu i dwoma własnymi przyciskami w układzie: Zagraj ponownie i Menu. Builder nie dodaje żadnych przycisków. Okno ma zaokrąglone tło z kształtu (lekcja 17).
pokaż wskazówkę
- Przyciski z układu obsłuż przez
widok.findViewById<Button>(…).setOnClickListener { okno.dismiss(); … }. Własne tło okna:okno.window?.setBackgroundDrawableResource(R.drawable.tlo_okno)poshow().
Lista trzech wydarzeń (nazwa, data, sala) w trzech kartach. Dotknięcie karty otwiera okno edycji z wypełnionymi polami tego wydarzenia; data wybierana przyciskiem otwierającym DatePickerDialog (okno w oknie). Po OK karta się aktualizuje. Okno ma też przycisk neutralny Usuń, który pyta o potwierdzenie drugim, zwykłym oknem.
pokaż wskazówkę
- Metoda
pokazEdycje(nr: Int)wypełnia pola wartościami z tablic przedshow(). Datę wybraną w kalendarzu trzymaj w zmiennej lokalnej okna i zapisz do tablicy dopiero po OK.
Co trzeba zapamiętać
- Środek okna projektujesz w
res/layout— bez tytułu i bez przycisków Buildera. inflateprzy każdym otwarciu, potem.setView(widok).- Kontrolki okna szukasz przez
widok.findViewById— to zmienne lokalne metody. - Okno sprawdzające dane:
setPositiveButton("…", null),val okno = ….show(), potemokno.getButton(…).setOnClickListeneriokno.dismiss()przy sukcesie.
Dokumentacja: Okno z własnym układem, AlertDialog (androidx).