school-it.pl // android + kotlin // rozdział 1 // lekcja 03
ConstraintLayout i ScrollView
ConstraintLayout to układ, który Android Studio wstawia do każdego nowego projektu. Nie układa kontrolek po kolei jak LinearLayout — każdej kontrolce mówisz, do czego jest przyczepiona: „pod tytułem”, „przy prawej krawędzi”, „obok pola Imię”. Dzięki temu skomplikowany ekran nie wymaga wielu zagnieżdżonych układów. Na koniec włożysz formularz do ScrollView, żeby dało się go przewinąć, gdy nie mieści się na ekranie.
Czego się dziś nauczysz
- Przyczepisz kontrolkę do krawędzi ekranu i do innej kontrolki.
- Rozciągniesz kontrolkę między dwoma przyczepami (
0dp) i wyśrodkujesz ją. - Postawisz dwie kontrolki obok siebie z równą szerokością.
- Umieścisz długi formularz w
ScrollView. - Rozpoznasz pozycje „tylko dla edytora”, które nie działają na telefonie.
Kontrolki na sznurkach
Wyobraź sobie, że każda kontrolka wisi na gumowych sznurkach przyczepionych do krawędzi ekranu albo do innych kontrolek. Taki sznurek to constraint (ograniczenie, przyczep). Zasada jest jedna:
Każda kontrolka potrzebuje co najmniej dwóch przyczepów
Jednego w poziomie (lewa lub prawa strona) i jednego w pionie (góra lub dół). Kontrolka bez przyczepu w danym kierunku ląduje na telefonie przy lewej lub górnej krawędzi — nawet jeśli w edytorze stała gdzie indziej.
pole E-mail jest przyczepione do obu boków, więc ma pełną szerokość; przycisk tylko do prawego, więc stoi przy prawej krawędzi
Jak zapisać constraint
Przyczep to atrybut o nazwie zbudowanej według wzoru app:layout_constraintMojaKrawędź_toKrawędźCeluOf="cel". Celem jest parent (układ) albo identyfikator innej kontrolki:
<EditText
android:id="@+id/etEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/tvTytul"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
| Atrybut | Czytamy |
|---|---|
layout_constraintTop_toBottomOf="@id/tvTytul" | Moja góra przyczepiona do dołu tytułu — jestem pod tytułem. |
layout_constraintTop_toTopOf="parent" | Moja góra do góry układu — jestem na samej górze. |
layout_constraintStart_toStartOf="parent" | Mój lewy bok do lewego boku układu. |
layout_constraintEnd_toEndOf="parent" | Mój prawy bok do prawego boku układu. |
layout_constraintStart_toEndOf="@id/etImie" | Mój lewy bok do prawego boku pola Imię — jestem na prawo od niego. |
layout_constraintBottom_toBottomOf="parent" | Mój dół do dołu układu — jestem na dole. |
Trzy rzeczy do zapamiętania:
- Przyczepy mają przedrostek
app:, a nieandroid:. Główny element musi mieć w nagłówkuxmlns:app="http://schemas.android.com/apk/res-auto"— szablon dodaje go sam. - Do innej kontrolki odwołujesz się przez
@id/nazwa(bez plusa) — plus jest tylko tam, gdzie identyfikator powstaje. - Margines działa tylko po stronie, która ma przyczep:
layout_marginTopzadziała, gdy jest przyczepTop_….
0dp i wyśrodkowanie
Gdy kontrolka ma przyczepy z obu stron (np. lewy i prawy do parent), sznurki ciągną ją w dwie strony. Co się stanie, zależy od szerokości:
| layout_width | Efekt przy przyczepach z obu stron |
|---|---|
wrap_content | Kontrolka ma szerokość treści i stoi na środku między przyczepami. |
0dp | Kontrolka rozciąga się od przyczepu do przyczepu (match constraints). |
match_parent | Nie używaj w ConstraintLayout — zamiast tego 0dp z przyczepami do parent. |
Tak samo działa to w pionie: przyczep góry do góry parent i dołu do dołu parent ustawi kontrolkę na środku ekranu. Szablon „Hello World!” robi dokładnie to — ma cztery przyczepy do parent.
Dwa różne znaczenia 0dp
W LinearLayout (lekcja 02) 0dp działało razem z wagą. W ConstraintLayout 0dp znaczy „rozciągnij między przyczepami”. Ta sama wartość, inny rodzic — inna reguła. Zawsze sprawdzaj, w jakim układzie jest kontrolka.
Dwie kontrolki obok siebie
Pola Imię i Nazwisko mają stać w jednym wierszu, każde na pół szerokości. Przyczepiamy je do siebie nawzajem: lewe pole kończy się tam, gdzie zaczyna się prawe, a prawe zaczyna tam, gdzie kończy się lewe. Takie wzajemne przyczepienie to łańcuch (chain). Obie kontrolki z szerokością 0dp dzielą miejsce po równo:
<EditText
android:id="@+id/etImie"
android:layout_width="0dp"
android:layout_marginEnd="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/etNazwisko" … />
<EditText
android:id="@+id/etNazwisko"
android:layout_width="0dp"
app:layout_constraintStart_toEndOf="@id/etImie"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/etImie" … />
Drugie pole ma górę przyczepioną do góry pierwszego (Top_toTopOf) — dzięki temu oba stoją równo w jednym wierszu.
Edytor Design
Przyczepy można tworzyć myszką: w trybie Design zaznacz kontrolkę i przeciągnij kółko z jej boku do krawędzi ekranu lub innej kontrolki. Android Studio dopisze atrybut app:layout_constraint… do XML. To wygodne, ale po każdym przeciągnięciu sprawdź kod w trybie Split.
tools:layout_editor_absoluteX — pułapka
Jeśli upuścisz kontrolkę w edytorze bez przyczepiania, Android Studio zapisze tools:layout_editor_absoluteX="120dp". Wszystko z przedrostkiem tools: działa tylko w podglądzie. Na telefonie taka kontrolka przeskoczy do lewego górnego rogu. Edytor ostrzega przed tym komunikatem This view is not constrained.
ScrollView — przewijanie
Żaden układ sam z siebie nie przewija treści. Gdy formularz jest wyższy od ekranu (mały telefon, obrót, otwarta klawiatura), dolne kontrolki są po prostu niedostępne. Rozwiązanie: włożyć cały formularz w ScrollView.
| Zasada | Dlaczego |
|---|---|
ScrollView ma dokładnie jedno dziecko | Przewija jedną rzecz. To dziecko to zwykle układ (ConstraintLayout lub LinearLayout), w którym jest reszta. |
dziecko ma layout_height="wrap_content" | Musi być tak wysokie jak treść — inaczej nie będzie czego przewijać. |
android:fillViewport="true" | Gdy treść jest krótka, dziecko i tak wypełnia cały ekran. |
| przewija w pionie | Do przewijania w poziomie jest osobny HorizontalScrollView. |
Zapisy na warsztaty
Formularz zapisu na szkolne warsztaty z programowania: tytuł, opis, imię i nazwisko w jednym wierszu, e-mail, długi regulamin i przycisk. Wszystko w ConstraintLayout wewnątrz ScrollView. Pola tekstowe (EditText) dokładnie poznasz w lekcji 06, a obsługę przycisku w lekcji 05 — tu zajmujemy się tylko układem.
palcem w górę
4. Uczestnicy otrzymają zaświadczenie o udziale.
5. Organizator może zmienić termin z ważnych powodów — uczestnicy dostaną informację e-mailem.
6. Zapisując się, akceptujesz regulamin pracowni komputerowej.
na wysokim telefonie formularz może zmieścić się w całości — sprawdź wtedy przewijanie po obróceniu ekranu
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Warsztaty, Kotlin, API 24. - Podmień
activity_main.xmlistrings.xml.MainActivity.kt— wersja kursowa (samsetContentView). - W
AndroidManifest.xmldopisz do istniejącego znacznika<activity android:name=".MainActivity" …>atrybutandroid:windowSoftInputMode="adjustResize"— ekran zmniejszy się, gdy otworzy się klawiatura, i da się przewinąć do przycisku.
<resources>
<string name="app_name">Warsztaty</string>
<string name="tytul">Warsztaty: Android od zera</string>
<string name="opis">Sobota, 14 listopada, sala 21. Zapisz się — liczba miejsc ograniczona.</string>
<string name="imie">Imię</string>
<string name="nazwisko">Nazwisko</string>
<string name="email">E-mail</string>
<string name="regulamin_tytul">Regulamin</string>
<string name="regulamin">1. Warsztaty są bezpłatne i przeznaczone dla uczniów szkoły.\n\n2. Każdy uczestnik przynosi własny laptop z zainstalowanym Android Studio. Szkoła może wypożyczyć kilka laptopów — zapytaj w sekretariacie.\n\n3. Zgłoszenie można odwołać najpóźniej dzień przed warsztatami.\n\n4. Uczestnicy otrzymają zaświadczenie o udziale.\n\n5. Organizator może zmienić termin z ważnych powodów — uczestnicy dostaną informację e-mailem.\n\n6. Zapisując się, akceptujesz regulamin pracowni komputerowej.</string>
<string name="zapisz">Zapisz się</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!-- ScrollView: przewija swoje JEDNO dziecko -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:fitsSystemWindows="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<!-- na górze, rozciągnięty między bokami -->
<TextView
android:id="@+id/tvTytul"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/tytul"
android:textSize="26sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- pod tytułem -->
<TextView
android:id="@+id/tvOpis"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/opis"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/tvTytul"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Imię i Nazwisko: łańcuch, każde pole na pół szerokości -->
<EditText
android:id="@+id/etImie"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:hint="@string/imie"
android:inputType="textPersonName"
app:layout_constraintTop_toBottomOf="@id/tvOpis"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/etNazwisko" />
<EditText
android:id="@+id/etNazwisko"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/nazwisko"
android:inputType="textPersonName"
app:layout_constraintTop_toTopOf="@id/etImie"
app:layout_constraintStart_toEndOf="@id/etImie"
app:layout_constraintEnd_toEndOf="parent" />
<!-- pod polem Imię, na całą szerokość -->
<EditText
android:id="@+id/etEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/email"
android:inputType="textEmailAddress"
app:layout_constraintTop_toBottomOf="@id/etImie"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/tvRegulaminTytul"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/regulamin_tytul"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@id/etEmail"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tvRegulamin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/regulamin"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/tvRegulaminTytul"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btnZapisz"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/zapisz"
app:layout_constraintTop_toBottomOf="@id/tvRegulamin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<!-- intent-filter z szablonu zostaje bez zmian -->
Co dzieje się po kolei
ScrollViewjest głównym elementem i ma jedno dziecko —ConstraintLayoutz wysokościąwrap_content. To dziecko jest tak wysokie jak cały formularz, aScrollViewpokazuje jego wycinek i pozwala przewijać.- Padding 16dp jest na
ConstraintLayout, a nie naScrollView— tam padding zajmujefitsSystemWindows. - Tytuł przyczepiony jest do góry układu, a każda kolejna kontrolka — do dołu poprzedniej (
Top_toBottomOf). Powstaje pionowy „łańcuszek” zależności: przesunięcie jednej kontrolki przesuwa wszystkie pod nią. - Prawie wszystko ma
0dpz przyczepami do obu bokówparent— rozciąga się na całą szerokość. Tytuł regulaminu mawrap_contenti tylko lewy przyczep — stoi przy lewej krawędzi. - Pola Imię i Nazwisko tworzą łańcuch poziomy i dzielą wiersz po równo.
- Cała kolumna przyczepów zaczyna się od
@id/tvTytul. Gdy zmienisz nazwę identyfikatora, zmień ją też we wszystkich odwołaniach (najprościej: Shift+F6 na nazwie — Rename).
Sprawdź trzy rzeczy
- Kliknij pole E-mail. Otworzy się klawiatura — przewiń formularz, aż zobaczysz przycisk Zapisz się nad klawiaturą.
- Usuń z pola E-mail przyczep
Top_toBottomOf. Edytor podkreśli pole na czerwono, a na telefonie pole wskoczy na samą górę, na tytuł. - Zmień szerokość pola E-mail z
0dpnawrap_content. Pole zrobi się wąskie i stanie na środku — bo jest przyczepione do obu boków.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<androidx.constraintlayout.widget.ConstraintLayout> | kontener | Układ, w którym położenie wyznaczają przyczepy. |
app:layout_constraintTop_toTopOf / _toBottomOf | atrybut | Przyczep górnej krawędzi do góry / dołu celu. |
app:layout_constraintBottom_toBottomOf / _toTopOf | atrybut | Przyczep dolnej krawędzi. |
app:layout_constraintStart_toStartOf / _toEndOf | atrybut | Przyczep lewej krawędzi. |
app:layout_constraintEnd_toEndOf / _toStartOf | atrybut | Przyczep prawej krawędzi. |
parent, @id/nazwa | cel | Układ albo inna kontrolka. |
layout_width="0dp" w ConstraintLayout | wartość | Rozciągnij między przyczepami. |
xmlns:app="…/res-auto" | przestrzeń nazw | Pozwala używać atrybutów app:. |
<ScrollView> | kontener | Przewija w pionie jedno dziecko. |
android:fillViewport="true" | atrybut | Krótka treść i tak wypełnia ekran. |
android:windowSoftInputMode="adjustResize" | atrybut manifestu | Ekran zmniejsza się, gdy pojawia się klawiatura. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| w podglądzie dobrze, na telefonie kontrolka w lewym górnym rogu | Brak przyczepów — zostało tylko tools:layout_editor_absoluteX/Y. Dodaj przyczep w poziomie i w pionie. |
android:layout_constraintTop_toTopOf | Atrybut ignorowany. Przyczepy mają przedrostek app:. |
| Namespace 'app’ is not bound | Brak xmlns:app="http://schemas.android.com/apk/res-auto" w głównym elemencie. |
@+id/tvTytul w przyczepie innej kontrolki | Nie zawsze błąd, ale łatwo o literówkę, która utworzy nowy, pusty identyfikator. W przyczepach pisz @id/. |
layout_marginTop nie działa | Kontrolka nie ma przyczepu Top_…. Margines działa tylko od strony przyczepu. |
| ScrollView can host only one direct child | Kilka kontrolek bezpośrednio w ScrollView. Obejmij je jednym układem. |
formularz w ScrollView się nie przewija | Dziecko ma layout_height="match_parent". Ustaw wrap_content. |
| dwie kontrolki nakładają się na siebie | Obie przyczepione do tego samego celu tą samą krawędzią, np. obie Top_toBottomOf="@id/tvOpis". Druga ma być pod pierwszą. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity, w ConstraintLayout, z tekstami w strings.xml. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Pięć przycisków o szerokości wrap_content: w lewym górnym rogu, w prawym górnym, na środku ekranu, w lewym dolnym i w prawym dolnym. Każdy ma napis z nazwą swojego miejsca. Sprawdź, czy po obróceniu telefonu przyciski nadal są w rogach.
pokaż wskazówkę
- Przycisk na środku ma cztery przyczepy do
parent. Przyciski w rogach — po dwa.
Tytuł „Logowanie” wyśrodkowany w poziomie, pod nim pola Login i Hasło (inputType="textPassword") na całą szerokość, pod nimi przycisk Zaloguj na całą szerokość, a pod przyciskiem, przy prawej krawędzi, napis „Nie pamiętam hasła”.
pokaż wskazówkę
- Tytuł:
wrap_content+ przyczepy z obu boków. Napis przy prawej krawędzi:wrap_contenti tylko przyczepEnd_toEndOf="parent".
Rozbuduj formularz warsztatów: pod e-mailem dodaj wiersz z trzema polami Kod pocztowy, Miasto, Ulica o równej szerokości i pod nim pole Telefon (inputType="phone"). Pamiętaj, żeby przepiąć tytuł regulaminu pod nowe pole.
pokaż wskazówkę
- Łańcuch trzech kontrolek: środkowa ma
Start_toEndOfpierwszej iEnd_toStartOftrzeciej. Wszystkie trzy0dp.
Ekran z tytułem „Regulamin”, bardzo długim tekstem (co najmniej trzy ekrany) i przyciskiem Akceptuję. Tekst się przewija, ale przycisk ma być zawsze widoczny na dole ekranu, poza przewijaną częścią.
pokaż wskazówkę
- Tym razem
ScrollViewnie jest głównym elementem. Główny jestConstraintLayout: przycisk przyczepiony do dołu, aScrollViewz wysokością0dpmiędzy tytułem a przyciskiem (Top_toBottomOftytułu,Bottom_toTopOfprzycisku).
Odtwórz kartę ucznia z lekcji 02 tylko w jednym ConstraintLayout — bez żadnego zagnieżdżonego LinearLayout. Trzy kafelki statystyk mają mieć równe szerokości, a przyciski mają być przyczepione do dołu ekranu.
pokaż wskazówkę
- Trzy kafelki to łańcuch trzech kontrolek
0dp. Pole uwag:layout_height="0dp", góra pod tytułem „Uwagi”, dół nad przyciskami.
Ankieta „Jak oceniasz stołówkę?” z co najmniej dwunastoma pytaniami. Każde pytanie to napis z numerem i pod nim pole odpowiedzi. Na górze, poza przewijaniem, pasek z nazwą ankiety; na dole, też poza przewijaniem, wiersz z przyciskami Wyczyść (1/3 szerokości) i Wyślij (2/3 szerokości). Ekran musi dać się przewinąć do ostatniego pytania także przy otwartej klawiaturze i w orientacji poziomej.
pokaż wskazówkę
- Główny
ConstraintLayoutz trzema częściami: pasek u góry,ScrollViewz0dpw środku, wiersz przycisków na dole. Proporcję 1 : 2 w łańcuchu ustawisz atrybutemapp:layout_constraintHorizontal_weight— działa jak waga wLinearLayout.
Co trzeba zapamiętać
- W
ConstraintLayoutkontrolka potrzebuje przyczepu w poziomie i w pionie; bez nich trafi do lewego górnego rogu. - Przyczep:
app:layout_constraintKrawędź_toKrawędźCeluOf="parent"albo="@id/inna". - Przyczepy z obu stron +
wrap_content= środek; +0dp= rozciągnięcie. - Dwie kontrolki przyczepione do siebie nawzajem tworzą łańcuch i dzielą miejsce.
ScrollViewma jedno dziecko o wysokościwrap_content; atrybutytools:działają tylko w podglądzie.
Dokumentacja: ConstraintLayout, ScrollView.