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

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.

XML ConstraintLayout constraint 0dp łańcuch ScrollView 3 × 45 min
CEL LEKCJI

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.
TEORIA

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.

12:30
Zapisy
↑ góra → parent · ← lewa → parent
E-mail
↑ góra → pod „Zapisy” · ←→ parent
Wyślij
↑ góra → pod „E-mail” · → prawa → parent
czerwone opisy to przyczepy każdej kontrolki

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

TEORIA

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:

pole E-mail z rysunku
<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" />
AtrybutCzytamy
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 nie android:. Główny element musi mieć w nagłówku xmlns: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_marginTop zadziała, gdy jest przyczep Top_….
TEORIA

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_widthEfekt przy przyczepach z obu stron
wrap_contentKontrolka ma szerokość treści i stoi na środku między przyczepami.
0dpKontrolka rozciąga się od przyczepu do przyczepu (match constraints).
match_parentNie używaj w ConstraintLayout — zamiast tego 0dp z przyczepami do parent.
12:30
wrap_content
przyczepy z obu stron → środek
0dp
przyczepy z obu stron → cała szerokość
ten sam przycisk, dwie szerokości

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.

TEORIA

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:

dwa pola w łańcuchu
<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.

TEORIA

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.

TEORIA

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.

ZasadaDlaczego
ScrollView ma dokładnie jedno dzieckoPrzewija 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 pionieDo przewijania w poziomie jest osobny HorizontalScrollView.
PRZYKŁAD

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.

12:30
Warsztaty: Android od zera
Sobota, 14 listopada, sala 21. Zapisz się — liczba miejsc ograniczona.
Imię
Nazwisko
E-mail
Regulamin
1. Warsztaty są bezpłatne i przeznaczone dla uczniów szkoły.
2. Każdy uczestnik przynosi własny laptop z zainstalowanym Android Studio. Szkoła może wypożyczyć kilka laptopów — zapytaj w sekretariacie.
3. Zgłoszenie można odwołać…
po uruchomieniu — przycisk jest niżej, trzeba przewinąć
→przewinięcie
palcem w górę
12:30
…3. Zgłoszenie można odwołać najpóźniej dzień przed warsztatami.
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.
Zapisz się
koniec formularza z przyciskiem

na wysokim telefonie formularz może zmieścić się w całości — sprawdź wtedy przewijanie po obróceniu ekranu

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Warsztaty, Kotlin, API 24.
  2. Podmień activity_main.xml i strings.xml. MainActivity.kt — wersja kursowa (sam setContentView).
  3. W AndroidManifest.xml dopisz do istniejącego znacznika <activity android:name=".MainActivity" …> atrybut android:windowSoftInputMode="adjustResize" — ekran zmniejszy się, gdy otworzy się klawiatura, i da się przewinąć do przycisku.
res/values/strings.xml — pełny plik
<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>
res/layout/activity_main.xml — pełny plik
<?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>
AndroidManifest.xml — istniejący znacznik activity, dopisany jeden atrybut
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:windowSoftInputMode="adjustResize">
    <!-- intent-filter z szablonu zostaje bez zmian -->

Co dzieje się po kolei

  • ScrollView jest głównym elementem i ma jedno dziecko — ConstraintLayout z wysokością wrap_content. To dziecko jest tak wysokie jak cały formularz, a ScrollView pokazuje jego wycinek i pozwala przewijać.
  • Padding 16dp jest na ConstraintLayout, a nie na ScrollView — tam padding zajmuje fitsSystemWindows.
  • 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 0dp z przyczepami do obu boków parent — rozciąga się na całą szerokość. Tytuł regulaminu ma wrap_content i 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

  1. Kliknij pole E-mail. Otworzy się klawiatura — przewiń formularz, aż zobaczysz przycisk Zapisz się nad klawiaturą.
  2. 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ł.
  3. Zmień szerokość pola E-mail z 0dp na wrap_content. Pole zrobi się wąskie i stanie na środku — bo jest przyczepione do obu boków.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<androidx.constraintlayout.widget.ConstraintLayout>kontenerUkład, w którym położenie wyznaczają przyczepy.
app:layout_constraintTop_toTopOf / _toBottomOfatrybutPrzyczep górnej krawędzi do góry / dołu celu.
app:layout_constraintBottom_toBottomOf / _toTopOfatrybutPrzyczep dolnej krawędzi.
app:layout_constraintStart_toStartOf / _toEndOfatrybutPrzyczep lewej krawędzi.
app:layout_constraintEnd_toEndOf / _toStartOfatrybutPrzyczep prawej krawędzi.
parent, @id/nazwacelUkład albo inna kontrolka.
layout_width="0dp" w ConstraintLayoutwartośćRozciągnij między przyczepami.
xmlns:app="…/res-auto"przestrzeń nazwPozwala używać atrybutów app:.
<ScrollView>kontenerPrzewija w pionie jedno dziecko.
android:fillViewport="true"atrybutKrótka treść i tak wypełnia ekran.
android:windowSoftInputMode="adjustResize"atrybut manifestuEkran zmniejsza się, gdy pojawia się klawiatura.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
w podglądzie dobrze, na telefonie kontrolka w lewym górnym roguBrak przyczepów — zostało tylko tools:layout_editor_absoluteX/Y. Dodaj przyczep w poziomie i w pionie.
android:layout_constraintTop_toTopOfAtrybut ignorowany. Przyczepy mają przedrostek app:.
Namespace 'app’ is not boundBrak xmlns:app="http://schemas.android.com/apk/res-auto" w głównym elemencie.
@+id/tvTytul w przyczepie innej kontrolkiNie zawsze błąd, ale łatwo o literówkę, która utworzy nowy, pusty identyfikator. W przyczepach pisz @id/.
layout_marginTop nie działaKontrolka nie ma przyczepu Top_…. Margines działa tylko od strony przyczepu.
ScrollView can host only one direct childKilka kontrolek bezpośrednio w ScrollView. Obejmij je jednym układem.
formularz w ScrollView się nie przewijaDziecko ma layout_height="match_parent". Ustaw wrap_content.
dwie kontrolki nakładają się na siebieObie przyczepione do tego samego celu tą samą krawędzią, np. obie Top_toBottomOf="@id/tvOpis". Druga ma być pod pierwszą.
ZADANIA

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.

ZAD 1Pięć przycisków★☆☆

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.
ZAD 2Ekran logowania★☆☆

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_content i tylko przyczep End_toEndOf="parent".
ZAD 3Adres w trzech polach★★☆

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_toEndOf pierwszej i End_toStartOf trzeciej. Wszystkie trzy 0dp.
ZAD 4Przycisk zawsze na dole★★☆

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 ScrollView nie jest głównym elementem. Główny jest ConstraintLayout: przycisk przyczepiony do dołu, a ScrollView z wysokością 0dp między tytułem a przyciskiem (Top_toBottomOf tytułu, Bottom_toTopOf przycisku).
ZAD 5Karta ucznia na nowo★★☆

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.
ZAD 6Ankieta szkolna★★★

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 ConstraintLayout z trzema częściami: pasek u góry, ScrollView z 0dp w środku, wiersz przycisków na dole. Proporcję 1 : 2 w łańcuchu ustawisz atrybutem app:layout_constraintHorizontal_weight — działa jak waga w LinearLayout.
PODSUMOWANIE

Co trzeba zapamiętać

  • W ConstraintLayout kontrolka 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.
  • ScrollView ma jedno dziecko o wysokości wrap_content; atrybuty tools: działają tylko w podglądzie.

Dokumentacja: ConstraintLayout, ScrollView.

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