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

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.

Kotlin XML shape solid / stroke corners gradient GradientDrawable 2 × 45 min
CEL LEKCJI

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

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.

res/drawable/tlo_karta.xml — biała karta z zaokrąglonymi rogami i szarą ramką
<?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.

TEORIA

Elementy kształtu

ElementAtrybutyEfekt
<solid>colorJednolite wypełnienie.
<stroke>width, color, dashWidth, dashGapRamka; z dashWidth i dashGap — przerywana.
<corners>radius albo topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadiusZaokrąglenie wszystkich lub wybranych rogów.
<gradient>startColor, centerColor, endColor, angle, typePrzejście kolorów zamiast solid.
<padding>left, top, right, bottomOdstęp treści kontrolki od krawędzi kształtu.
<size>width, heightRozmiar domyślny — ważny przy kształtach używanych jako ikona.
12:30
solid
solid + corners
stroke + corners
stroke z dashWidth
gradient angle=”0″
gradient angle=”90″
gradient type=”radial”
tylko górne rogi
elementy <shape> i ich połączenia

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

TEORIA

Prostokąt, owal, linia

android:shapeKształt
rectangleProstokąt (domyślnie). Z corners — zaokrąglony; przy promieniu równym połowie wysokości — pigułka.
ovalOwal wpisany w kontrolkę. W kwadratowej kontrolce — koło.
linePozioma linia przez środek; rysuje ją stroke. Kontrolka musi mieć wysokość większą niż grubość linii.
ringPierścień — rzadko używany.
res/drawable/kolo.xml — koło na awatar
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#6750A4" />
</shape>
TEORIA

Kształt jako tło

Jeden kształt może być tłem wielu kontrolek — TextView, ImageView, układu, a nawet całego ekranu:

okrągły awatar z inicjałami
<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.

TEORIA

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óbKiedy
atrybuty przycisku Material: app:cornerRadius, app:strokeColor, app:strokeWidth, android:backgroundTintWystarczy 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.
dwa przyciski
<!-- 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).

TEORIA

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:

zmiana koloru koła
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
FragmentZnaczenie
tvAwatar.backgroundTł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.
PRZYKŁAD

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.

12:30
AN
Anna Nowak
klasa 3TP · technik programista
Kotlin robotyka gry
Dołącz do koła
każdy element to inny kształt XML
→dotknięcie
awatara
12:30
AN
Anna Nowak
klasa 3TP · technik programista
Kotlin robotyka gry
Dołącz do koła
awatar w nowym kolorze — kształt i ramka zostały

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Profil, Kotlin, API 24.
  2. Utwórz w res/drawable sześć plików kształtów (Root element: shape) i wklej do nich kod poniżej.
  3. Podmień activity_main.xml i MainActivity.kt.
res/drawable/
├── 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
res/drawable/tlo_naglowek.xml
<?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>
res/drawable/tlo_awatar.xml
<?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>
res/drawable/tlo_karta.xml
<?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>
res/drawable/tlo_etykieta.xml
<?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>
res/drawable/linia_przerywana.xml
<?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>
res/drawable/tlo_przycisk.xml
<?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>
res/layout/activity_main.xml — pełny plik
<?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>
MainActivity.kt — pełny plik
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 padding zapisany 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ły Button.
  • W kodzie mutate() robi osobną kopię kształtu awatara, a setColor zmienia tylko wypełnienie — ramka i kształt zostają.

Sprawdź trzy rzeczy

  1. Zmień w kodzie ksztaltAwatara.setColor(…) na tvAwatar.setBackgroundColor(…). Po dotknięciu awatar zrobi się kwadratowy — tło zostało zastąpione.
  2. Zamień AppCompatButton na Button. Gradient zniknie pod kolorem motywu.
  3. Zmień szerokość awatara na wrap_content. Koło stanie się owalem dopasowanym do napisu.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<shape android:shape="…">drawableKształt: rectangle, oval, line, ring.
<solid>, <stroke>elementyWypełnienie, ramka (z dashWidth/dashGap przerywana).
<corners>elementZaokrąglenie rogów.
<gradient>elementPrzejście kolorów; angle co 45°.
<padding>, <size>elementyOdstęp treści, rozmiar domyślny.
android:background="@drawable/…"atrybutKształt jako tło.
<View>kontrolkaPusty prostokąt — na tło, linię, odstęp.
<androidx.appcompat.widget.AppCompatButton>kontrolkaPrzycisk z w pełni własnym tłem.
app:cornerRadius, app:strokeColor, app:strokeWidthatrybutyZaokrąglenie i ramka przycisku Material.
view.background.mutate() as GradientDrawablekodDostęp do kształtu z kodu.
setColor(…), setStroke(px, kolor)metodyZmiana wypełnienia i ramki.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
kształt na <Button> jest fioletowyPrzycisk Material przemalowuje tło. Użyj AppCompatButton albo atrybutów app:cornerRadius.
linia (shape="line") niewidocznaBrak stroke albo wysokość kontrolki mniejsza niż grubość linii.
koło wychodzi jako owalSzerokość 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 kontrolkiBrak mutate() — wszystkie kontrolki dzielą jeden obiekt kształtu.
setBackgroundColor na kontrolce z kształtemKształt zastąpiony prostokątem. Zmieniaj kolor przez GradientDrawable.
<solid> i <gradient> w jednym kształcieDziała tylko jedno z nich. Wybierz wypełnienie albo gradient.
ZADANIA

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.

ZAD 1Galeria kształtów★☆☆

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: topLeftRadius i bottomLeftRadius.
ZAD 2Dymki czatu★☆☆

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" i layout_gravity="end" w pionowym LinearLayout.
ZAD 3Sygnalizator z kół★★☆

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().
ZAD 4Bilet na koncert★★☆

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.
ZAD 5Wskaźnik siły hasła★★☆

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.
ZAD 6Edytor przycisku★★★

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)).
PODSUMOWANIE

Co trzeba zapamiętać

  • Kształt to plik XML w res/drawable z elementem <shape>; używasz go jako android:background.
  • solid — wypełnienie, stroke — ramka, corners — zaokrąglenie, gradient — przejście kolorów.
  • Koło: shape="oval" w kwadratowej kontrolce; pigułka: corners z dużym promieniem.
  • Przycisk z własnym kształtem: AppCompatButton; proste zaokrąglenie: app:cornerRadius.
  • Kolor kształtu z kodu: background.mutate() as GradientDrawable i setColor.

Dokumentacja: Shape drawable, GradientDrawable.

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