school-it.pl // android + kotlin // rozdział 5 // lekcja 17
Shape — kształty w XML
Do tej pory tło kontrolki mogło być tylko jednolitym prostokątem w jednym kolorze. Karty z zaokrąglonymi rogami, okrągłe awatary, ramki, gradienty, przyciski w kształcie pigułki — wszystko to robi się bez programu graficznego, plikiem XML z elementem <shape>. W tej lekcji zaprojektujesz kształty, użyjesz ich jako tła kontrolek i zmienisz kolor kształtu z kodu.
Czego się dziś nauczysz
- Utworzysz plik kształtu w
res/drawable. - Ustawisz wypełnienie, ramkę (także przerywaną), zaokrąglenie i gradient.
- Narysujesz prostokąt, owal i linię.
- Użyjesz kształtu jako tła napisu, układu i przycisku.
- Zmienisz kolor kształtu z kodu Kotlin.
Plik kształtu
Kształt to plik XML w folderze res/drawable — tym samym, w którym są obrazy (lekcja 07). Tworzysz go tak: prawy przycisk na drawable → New → Drawable Resource File → nazwa (np. tlo_karta) → w polu Root element wpisz shape → OK.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" /> <!-- wypełnienie -->
<stroke android:width="1dp" android:color="#CAC4D0" /> <!-- ramka -->
<corners android:radius="16dp" /> <!-- zaokrąglenie rogów -->
<padding android:left="16dp" android:top="16dp"
android:right="16dp" android:bottom="16dp" /> <!-- odstęp treści od krawędzi -->
</shape>
Kształt użyjesz jak każdy obraz z drawable: android:background="@drawable/tlo_karta". Podgląd kształtu widać po otwarciu pliku — po prawej stronie edytora.
Elementy kształtu
| Element | Atrybuty | Efekt |
|---|---|---|
<solid> | color | Jednolite wypełnienie. |
<stroke> | width, color, dashWidth, dashGap | Ramka; z dashWidth i dashGap — przerywana. |
<corners> | radius albo topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius | Zaokrąglenie wszystkich lub wybranych rogów. |
<gradient> | startColor, centerColor, endColor, angle, type | Przejście kolorów zamiast solid. |
<padding> | left, top, right, bottom | Odstęp treści kontrolki od krawędzi kształtu. |
<size> | width, height | Rozmiar domyślny — ważny przy kształtach używanych jako ikona. |
Kąt gradientu
android:angle podawaj jako wielokrotność 45 — inne wartości na części wersji Androida powodują błąd. 0 — od lewej do prawej, 90 — od dołu do góry, 270 — od góry do dołu. Gradient promieniowy (type="radial") wymaga dodatkowo android:gradientRadius, np. "120dp".
Prostokąt, owal, linia
| android:shape | Kształt |
|---|---|
rectangle | Prostokąt (domyślnie). Z corners — zaokrąglony; przy promieniu równym połowie wysokości — pigułka. |
oval | Owal wpisany w kontrolkę. W kwadratowej kontrolce — koło. |
line | Pozioma linia przez środek; rysuje ją stroke. Kontrolka musi mieć wysokość większą niż grubość linii. |
ring | Pierścień — rzadko używany. |
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#6750A4" />
</shape>
Kształt jako tło
Jeden kształt może być tłem wielu kontrolek — TextView, ImageView, układu, a nawet całego ekranu:
<TextView
android:layout_width="72dp"
android:layout_height="72dp"
android:background="@drawable/kolo"
android:gravity="center"
android:text="AN"
android:textColor="#FFFFFF"
android:textSize="28sp" />
Kształt dopasowuje się do rozmiaru kontrolki. Koło wyjdzie tylko wtedy, gdy szerokość i wysokość są równe — przy wrap_content wyjdzie owal.
Kształt na przycisku
W lekcji 05 było ostrzeżenie: przycisk Material 3 sam zarządza swoim tłem i ignoruje android:background (a dokładniej — przemalowuje go kolorem motywu). Masz dwie drogi:
| Sposób | Kiedy |
|---|---|
atrybuty przycisku Material: app:cornerRadius, app:strokeColor, app:strokeWidth, android:backgroundTint | Wystarczy zmienić zaokrąglenie, ramkę i kolor. Przycisk zachowuje efekt dotyku Material. |
znacznik <androidx.appcompat.widget.AppCompatButton> z android:background="@drawable/…" | Potrzebny zupełnie własny wygląd — gradient, przerywana ramka, nietypowe rogi. |
<!-- 1. przycisk Material z mniejszym zaokrągleniem i ramką -->
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Zapisz"
app:cornerRadius="6dp"
app:strokeColor="#1D1B20"
app:strokeWidth="2dp" />
<!-- 2. przycisk z tłem z własnego kształtu -->
<androidx.appcompat.widget.AppCompatButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/tlo_gradient"
android:text="Dołącz"
android:textColor="#FFFFFF" />
Atrybuty app: wymagają w głównym elemencie xmlns:app="http://schemas.android.com/apk/res-auto" (lekcja 03).
Kolor kształtu z kodu
Gdyby w kodzie wywołać setBackgroundColor, kształt zostałby zastąpiony zwykłym prostokątem — zniknęłyby zaokrąglenia i ramka. Żeby zmienić tylko kolor, trzeba dostać się do samego kształtu. W kodzie kształt z pliku XML jest obiektem klasy GradientDrawable:
val ksztalt = tvAwatar.background.mutate() as GradientDrawable
ksztalt.setColor(Color.parseColor("#2E7D32")) // nowe wypełnienie
ksztalt.setStroke(4, Color.parseColor("#1B5E20")) // nowa ramka: grubość w pikselach, kolor
| Fragment | Znaczenie |
|---|---|
tvAwatar.background | Tło kontrolki — tu: kształt z pliku kolo.xml. |
.mutate() | „Zrób osobną kopię dla tej kontrolki”. Bez tego zmiana koloru objęłaby wszystkie kontrolki z tym samym kształtem. |
as GradientDrawable | „To jest kształt” — dopiero wtedy dostępne są setColor i setStroke. |
Profil ucznia
Ekran profilu złożony wyłącznie z kształtów XML: nagłówek z gradientem i zaokrąglonymi dolnymi rogami, okrągły awatar z inicjałami, białą kartę z ramką, „etykiety” zainteresowań w kształcie pigułek, przerywaną linię oddzielającą i przycisk z gradientem. Dotknięcie awatara zmienia jego kolor na kolejny z listy.
awatara
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Profil, Kotlin, API 24. - Utwórz w
res/drawablesześć plików kształtów (Root element:shape) i wklej do nich kod poniżej. - Podmień
activity_main.xmliMainActivity.kt.
├── tlo_naglowek.xmlgradient, zaokrąglone dolne rogi
├── tlo_awatar.xmlkoło z białą ramką
├── tlo_karta.xmlbiała karta z ramką
├── tlo_etykieta.xmlpigułka
├── linia_przerywana.xmllinia
└── tlo_przycisk.xmlgradient-pigułka
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- od góry (ciemny) do dołu (jaśniejszy) -->
<gradient
android:angle="270"
android:endColor="#9A82DB"
android:startColor="#6750A4" />
<!-- zaokrąglone tylko dolne rogi -->
<corners
android:bottomLeftRadius="32dp"
android:bottomRightRadius="32dp" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#6750A4" />
<stroke
android:width="4dp"
android:color="#FFFFFF" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<stroke
android:width="1dp"
android:color="#CAC4D0" />
<corners android:radius="16dp" />
<padding
android:bottom="16dp"
android:left="16dp"
android:right="16dp"
android:top="16dp" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#E8DEF8" />
<!-- duży promień: końce zawsze półkoliste, jak pigułka -->
<corners android:radius="100dp" />
<padding
android:bottom="6dp"
android:left="14dp"
android:right="14dp"
android:top="6dp" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<stroke
android:width="2dp"
android:color="#CAC4D0"
android:dashGap="6dp"
android:dashWidth="10dp" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="0"
android:endColor="#7FD8FF"
android:startColor="#6750A4" />
<corners android:radius="100dp" />
</shape>
<?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:background="#F3EDF7"
android:fitsSystemWindows="true"
android:orientation="vertical">
<!-- nagłówek: pusty View z gradientem -->
<View
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="@drawable/tlo_naglowek" />
<!-- awatar: ujemny margines wsuwa go na nagłówek -->
<TextView
android:id="@+id/tvAwatar"
android:layout_width="104dp"
android:layout_height="104dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="-60dp"
android:background="@drawable/tlo_awatar"
android:gravity="center"
android:text="AN"
android:textColor="#FFFFFF"
android:textSize="36sp"
android:textStyle="bold" />
<!-- karta -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:background="@drawable/tlo_karta"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Anna Nowak"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="klasa 3TP · technik programista" />
<!-- linia: wysokość większa niż grubość kreski -->
<View
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/linia_przerywana" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/tlo_etykieta"
android:text="Kotlin" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/tlo_etykieta"
android:text="robotyka" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/tlo_etykieta"
android:text="gry" />
</LinearLayout>
</LinearLayout>
<!-- przycisk z własnym tłem: AppCompatButton -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnDolacz"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/tlo_przycisk"
android:text="Dołącz do koła"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center" />
</LinearLayout>
package pl.schoolit.profil
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tvAwatar = findViewById<TextView>(R.id.tvAwatar)
val tvInfo = findViewById<TextView>(R.id.tvInfo)
val btnDolacz = findViewById<Button>(R.id.btnDolacz) // AppCompatButton też jest Button
val kolory = arrayOf("#6750A4", "#2E7D32", "#C62828", "#EF6C00", "#00838F")
var nrKoloru = 0
// osobna kopia kształtu tylko dla awatara
val ksztaltAwatara = tvAwatar.background.mutate() as GradientDrawable
tvAwatar.setOnClickListener {
nrKoloru = (nrKoloru + 1) % kolory.size
ksztaltAwatara.setColor(Color.parseColor(kolory[nrKoloru]))
// ramka zostaje biała, bo zmieniamy tylko wypełnienie
}
btnDolacz.setOnClickListener {
tvInfo.text = "Wysłano zgłoszenie do koła robotyki"
}
}
}
Co dzieje się po kolei
- Nagłówek to pusty
View— kontrolka bez treści, tylko z tłem. Gradient idzie od góry do dołu (angle="270"), a zaokrąglone są tylko dolne rogi. - Awatar ma
layout_marginTop="-60dp". Ujemny margines przesuwa kontrolkę w górę, na nagłówek. Biała ramka kształtu oddziela koło od gradientu. - Karta ma
paddingzapisany w kształcie, więc w XML karty nie trzeba go powtarzać. - Wszystkie trzy etykiety korzystają z jednego pliku
tlo_etykieta.xml. Promień 100dp jest większy niż połowa wysokości etykiety, więc końce są zawsze półokrągłe. - Przycisk to
AppCompatButton— dzięki temu własne tło nie jest przemalowywane kolorem motywu. W kodzie traktujemy go jako zwykłyButton. - W kodzie
mutate()robi osobną kopię kształtu awatara, asetColorzmienia tylko wypełnienie — ramka i kształt zostają.
Sprawdź trzy rzeczy
- Zmień w kodzie
ksztaltAwatara.setColor(…)natvAwatar.setBackgroundColor(…). Po dotknięciu awatar zrobi się kwadratowy — tło zostało zastąpione. - Zamień
AppCompatButtonnaButton. Gradient zniknie pod kolorem motywu. - Zmień szerokość awatara na
wrap_content. Koło stanie się owalem dopasowanym do napisu.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<shape android:shape="…"> | drawable | Kształt: rectangle, oval, line, ring. |
<solid>, <stroke> | elementy | Wypełnienie, ramka (z dashWidth/dashGap przerywana). |
<corners> | element | Zaokrąglenie rogów. |
<gradient> | element | Przejście kolorów; angle co 45°. |
<padding>, <size> | elementy | Odstęp treści, rozmiar domyślny. |
android:background="@drawable/…" | atrybut | Kształt jako tło. |
<View> | kontrolka | Pusty prostokąt — na tło, linię, odstęp. |
<androidx.appcompat.widget.AppCompatButton> | kontrolka | Przycisk z w pełni własnym tłem. |
app:cornerRadius, app:strokeColor, app:strokeWidth | atrybuty | Zaokrąglenie i ramka przycisku Material. |
view.background.mutate() as GradientDrawable | kod | Dostęp do kształtu z kodu. |
setColor(…), setStroke(px, kolor) | metody | Zmiana wypełnienia i ramki. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
kształt na <Button> jest fioletowy | Przycisk Material przemalowuje tło. Użyj AppCompatButton albo atrybutów app:cornerRadius. |
linia (shape="line") niewidoczna | Brak stroke albo wysokość kontrolki mniejsza niż grubość linii. |
| koło wychodzi jako owal | Szerokość i wysokość kontrolki są różne. |
android:angle="30" | Na części wersji Androida awaria przy wyświetlaniu: <gradient> tag requires 'angle’ attribute to be a multiple of 45. Używaj wielokrotności 45. |
| zmiana koloru awatara zmienia też inne kontrolki | Brak mutate() — wszystkie kontrolki dzielą jeden obiekt kształtu. |
setBackgroundColor na kontrolce z kształtem | Kształt zastąpiony prostokątem. Zmieniaj kolor przez GradientDrawable. |
<solid> i <gradient> w jednym kształcie | Działa tylko jedno z nich. Wybierz wypełnienie albo gradient. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Cały kod pisz w onCreate. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Sześć napisów, każdy z innym kształtem tła: prostokąt z zaokrągleniem 8dp, koło, pigułka, ramka przerywana bez wypełnienia, gradient pionowy, prostokąt zaokrąglony tylko po lewej stronie. Napis w środku mówi, jaki to kształt.
pokaż wskazówkę
- Ramka bez wypełnienia: sam
<stroke>, bez<solid>. Lewa strona:topLeftRadiusibottomLeftRadius.
Fragment rozmowy: cztery wiadomości w dymkach. Wiadomości rozmówcy — szare, przy lewej krawędzi, bez zaokrąglenia lewego dolnego rogu. Twoje — fioletowe z białym tekstem, przy prawej, bez zaokrąglenia prawego dolnego rogu.
pokaż wskazówkę
- Dwa pliki kształtów. Położenie dymka:
layout_width="wrap_content"ilayout_gravity="end"w pionowymLinearLayout.
Ciemny prostokąt z zaokrąglonymi rogami, a w nim trzy koła jedno pod drugim. Przycisk Zmień przełącza światła: zapalone koło ma pełny kolor, zgaszone — ciemnoszary. Kolory zmieniaj przez GradientDrawable, nie przez podmianę pliku.
pokaż wskazówkę
- Wszystkie trzy koła mogą mieć ten sam plik kształtu, ale każde potrzebuje własnego
mutate().
Bilet jako karta z gradientowym nagłówkiem (nazwa koncertu), danymi (data, miejsce, sektor) i przerywaną linią „do oderwania”, pod którą jest numer biletu czcionką monospace w ramce. Przycisk Kup z gradientem i zaokrągleniem 12dp.
pokaż wskazówkę
- Nagłówek zaokrąglony tylko u góry, karta pod nim — tylko u dołu; ustaw je bezpośrednio jedno pod drugim bez marginesu.
Pole hasła i pod nim cztery krótkie, zaokrąglone paski obok siebie. Po kliknięciu Sprawdź zapala się 1–4 pasków w kolorze zależnym od siły (czerwony, pomarańczowy, żółty, zielony). Siła: +1 za długość ≥ 8, +1 za cyfrę, +1 za wielką literę, +1 za znak specjalny.
pokaż wskazówkę
- Cztery paski w tablicy, każdy z własnym
GradientDrawable. W pętli: pasek o numerze mniejszym niż siła dostaje kolor, pozostałe — szary.
Na górze przycisk-podgląd (AppCompatButton z kształtem). Pod nim trzy suwaki (lekcja 14): promień zaokrąglenia 0–40 dp, grubość ramki 0–10 dp, odcień koloru (0–360 — barwa z koła barw). Podgląd zmienia się w trakcie przesuwania, a na dole wyświetla się gotowy kod XML kształtu do skopiowania.
pokaż wskazówkę
- Zaokrąglenie z kodu:
ksztalt.cornerRadius = px. Kod przyjmuje piksele:dp * resources.displayMetrics.density. Kolor z barwy:Color.HSVToColor(floatArrayOf(barwa, 0.6f, 0.8f)).
Co trzeba zapamiętać
- Kształt to plik XML w
res/drawablez elementem<shape>; używasz go jakoandroid:background. solid— wypełnienie,stroke— ramka,corners— zaokrąglenie,gradient— przejście kolorów.- Koło:
shape="oval"w kwadratowej kontrolce; pigułka:cornersz dużym promieniem. - Przycisk z własnym kształtem:
AppCompatButton; proste zaokrąglenie:app:cornerRadius. - Kolor kształtu z kodu:
background.mutate() as GradientDrawableisetColor.
Dokumentacja: Shape drawable, GradientDrawable.