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

school-it.pl // android + kotlin // rozdział 2 // lekcja 07

ImageView — obrazy

ImageView wyświetla obraz: zdjęcie, ikonę, logo. W tej lekcji dodasz obrazy do projektu — zarówno zdjęcia, jak i ikony wektorowe — wyświetlisz je, dopasujesz do miejsca na ekranie i będziesz je zmieniać z kodu. Zbudujesz przeglądarkę pogody, w której przyciski przełączają ikonę, opis i kolor karty.

Kotlin XML ImageView drawable Vector Asset scaleType setImageResource 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Dodasz do projektu zdjęcie i ikonę wektorową.
  • Wyświetlisz obraz w ImageView z opisem dla czytników ekranu.
  • Dobierzesz sposób dopasowania obrazu atrybutem scaleType.
  • Zmienisz obraz z kodu metodą setImageResource.
  • Zapiszesz listę obrazów w tablicy i będziesz po niej przechodzić przyciskami.
TEORIA

Obrazy w projekcie

Obrazy trafiają do folderu res/drawable. Android Studio obsługuje dwa rodzaje:

RodzajCo to jestKiedy
bitmapa
.png .jpg .webp
Siatka kolorowych pikseli. Przy powiększaniu robi się rozmyta.Zdjęcia.
wektor
.xml
Opis kształtów (linie, łuki, wypełnienia). Ostry w każdym rozmiarze, plik zajmuje kilka kilobajtów.Ikony, proste grafiki.

Zdjęcie

  1. Skopiuj plik w Eksploratorze Windows (Ctrl+C).
  2. W Android Studio kliknij folder res/drawable i wklej (Ctrl+V). Wybierz folder drawable (bez dopisku -v24).
  3. Jeśli nazwa pliku ma wielkie litery, spacje lub polskie znaki — zmień ją: małe litery, cyfry i podkreślnik, np. klasa_3tp.jpg. Rozszerzenie nie jest częścią nazwy zasobu: w kodzie to R.drawable.klasa_3tp.

Ikona wektorowa

  1. Prawy przycisk na res → New → Vector Asset.
  2. Clip art → wybierz ikonę z tysięcy gotowych ikon Material (wyszukiwarka po angielsku: sunny, home, favorite…).
  3. Nadaj nazwę (np. ic_dom), ustaw rozmiar i kolor → Next → Finish. Plik ic_dom.xml pojawi się w res/drawable.

Uważaj na duże zdjęcia

Zdjęcie prosto z telefonu ma 12–50 megapikseli. Wyświetlenie go w ImageView może zamknąć aplikację błędem Canvas: trying to draw too large bitmap. Przed dodaniem zmniejsz zdjęcie do około 1000–1500 pikseli szerokości (np. w Paint: Zmień rozmiar).

TEORIA

ImageView w XML

obraz o stałej wysokości
<ImageView
    android:id="@+id/ivZdjecie"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:src="@drawable/klasa_3tp"
    android:scaleType="centerCrop"
    android:contentDescription="Zdjęcie klasy 3TP na wycieczce" />
AtrybutZnaczenie
android:srcObraz do wyświetlenia: @drawable/nazwa.
android:contentDescriptionOpis obrazu czytany osobom niewidomym przez czytnik ekranu (TalkBack). Dla obrazów czysto ozdobnych: @null.
android:scaleTypeJak dopasować obraz do rozmiaru kontrolki (niżej).
android:adjustViewBounds="true"Przy wrap_content wysokość dopasuje się do proporcji obrazu.
android:tintPrzemalowuje obraz na jeden kolor — przydatne dla ikon.
android:backgroundTło za obrazem (widoczne tam, gdzie obraz go nie zakrywa).

src a background

src to sam obraz — dopasowuje się według scaleType. background zawsze rozciąga się na całą kontrolkę. Obrazu nie ustawiaj jako background, bo się zniekształci.

TEORIA

Dopasowanie: scaleType

Obraz rzadko ma dokładnie takie proporcje jak miejsce na niego. scaleType decyduje, co zrobić z różnicą. Poniżej to samo poziome zdjęcie w kwadratowej kontrolce:

12:30
fitCenter (domyślny)
cały obraz, puste pasy
centerCrop
wypełnia, boki ucięte
fitXY
rozciągnięty, zniekształcony
center
bez skalowania, środek
poziome zdjęcie w kontrolce 96 × 96 dp
scaleTypeKiedy
fitCenterCały obraz musi być widoczny (logo, ikona). Wartość domyślna.
centerCropZdjęcia w kafelkach i nagłówkach — ładnie wypełniają miejsce, coś z brzegów może zniknąć.
fitXYPrawie nigdy — obraz traci proporcje.
centerMały obraz pokazany w oryginalnym rozmiarze.
TEORIA

Obraz z kodu

MainActivity.kt — fragment onCreate
val ivIkona = findViewById<ImageView>(R.id.ivIkona)

ivIkona.setImageResource(R.drawable.chmura)    // nowy obraz z res/drawable
ivIkona.contentDescription = "Pochmurno"      // nowy opis — razem z obrazem
ivIkona.alpha = 0.5f                          // przezroczystość: 0 = niewidoczny, 1 = pełny
ivIkona.rotation = 45f                        // obrót w stopniach

Kilka obrazów wygodnie trzymać w tablicy. R.drawable.slonce to liczba (numer zasobu), więc tablica jest tablicą liczb — intArrayOf:

obrazy w tablicy
val obrazki = intArrayOf(R.drawable.slonce, R.drawable.chmura, R.drawable.deszcz)
var nr = 0
ivIkona.setImageResource(obrazki[nr])

Przejście do następnego obrazu z powrotem na początek po ostatnim to reszta z dzielenia: nr = (nr + 1) % obrazki.size. Przy trzech obrazach nr przyjmuje kolejno 0, 1, 2, 0, 1, 2… Cofanie: nr = (nr - 1 + obrazki.size) % obrazki.size — dodanie rozmiaru chroni przed liczbą ujemną.

ImageView też może reagować na kliknięcie — ma setOnClickListener tak samo jak przycisk.

PRZYKŁAD

Przeglądarka pogody

Karta z dużą ikoną pogody, opisem i temperaturą. Przyciski ◀ i ▶ przechodzą po czterech rodzajach pogody — razem z ikoną zmienia się opis, temperatura i kolor karty. Dotknięcie ikony obraca ją o 15°. Ikony to pliki wektorowe dołączone niżej.

12:30
Pogoda w Lęborku
Słonecznie
24°C
1 / 4
◀
▶
start: pierwszy obraz
→▶
12:30
Pogoda w Lęborku
Pochmurno
17°C
2 / 4
◀
▶
drugi obraz, inny kolor karty
12:31
Pogoda w Lęborku
Pogodna noc
9°C
4 / 4
◀
▶
czwarty obraz, ikona dwa razy dotknięta (obrót 30°)

po czwartym obrazie ▶ wraca do pierwszego; ◀ na pierwszym przechodzi do ostatniego

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Pogoda, Kotlin, API 24.
  2. Utwórz w res/drawable cztery pliki wektorowe: prawy przycisk na drawable → New → Drawable Resource File → nazwa (np. slonce) → OK, a potem zastąp całą zawartość kodem poniżej.
  3. Podmień activity_main.xml i MainActivity.kt.
app/src/main/res/
├── drawable/
│   ├── slonce.xmlnowy
│   ├── chmura.xmlnowy
│   ├── deszcz.xmlnowy
│   └── ksiezyc.xmlnowy
└── layout/
    └── activity_main.xml
res/drawable/slonce.xml — pełny plik
<!-- wektor: rysunek 24 × 24 jednostki, wyświetlany domyślnie jako 96 × 96 dp -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="96dp"
    android:height="96dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <!-- tarcza: koło o promieniu 5 -->
    <path
        android:fillColor="#FFB300"
        android:pathData="M7,12a5,5 0 1,0 10,0a5,5 0 1,0 -10,0" />
    <!-- cztery promienie: prostokąty u góry, na dole, z lewej i z prawej -->
    <path
        android:fillColor="#FFB300"
        android:pathData="M11,1h2v3h-2z M11,20h2v3h-2z M1,11h3v2H1z M20,11h3v2h-3z" />
</vector>
res/drawable/chmura.xml — pełny plik
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="96dp"
    android:height="96dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="#90A4AE"
        android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96z" />
</vector>
res/drawable/deszcz.xml — pełny plik
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="96dp"
    android:height="96dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <!-- chmura zmniejszona do 80% i dosunięta do góry -->
    <group
        android:pivotX="12"
        android:pivotY="0"
        android:scaleX="0.8"
        android:scaleY="0.8">
        <path
            android:fillColor="#78909C"
            android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96z" />
    </group>
    <!-- trzy krople -->
    <path
        android:fillColor="#1E88E5"
        android:pathData="M7,18l-1,4h1.5l1,-4z M11.5,18l-1,4h1.5l1,-4z M16,18l-1,4h1.5l1,-4z" />
</vector>
res/drawable/ksiezyc.xml — pełny plik
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="96dp"
    android:height="96dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="#5C6BC0"
        android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9 9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36 -0.98,1.37 -2.58,2.26 -4.4,2.26 -2.98,0 -5.4,-2.42 -5.4,-5.4 0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3z" />
</vector>

Nie musisz rozumieć pathData

Ciąg liter i liczb w pathData to instrukcje rysowania („idź do punktu”, „narysuj łuk”…). Nikt nie pisze ich ręcznie — robi to Vector Asset albo program graficzny. Wystarczy wiedzieć, gdzie zmienić kolor: android:fillColor.

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:orientation="vertical"
    android:layout_margin="16dp"
    android:fitsSystemWindows="true">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="12dp"
        android:text="Pogoda w Lęborku"
        android:textSize="24sp"
        android:textStyle="bold" />

    <!-- karta: jej tło zmienia się z kodu -->
    <LinearLayout
        android:id="@+id/karta"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        android:padding="24dp">

        <ImageView
            android:id="@+id/ivPogoda"
            android:layout_width="160dp"
            android:layout_height="160dp"
            android:contentDescription="Słonecznie"
            android:scaleType="fitCenter"
            android:src="@drawable/slonce" />

        <TextView
            android:id="@+id/tvOpis"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:textSize="26sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/tvTemperatura"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="48sp" />

        <TextView
            android:id="@+id/tvNumer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnPoprzedni"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginEnd="8dp"
            android:text="◀" />

        <Button
            android:id="@+id/btnNastepny"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="▶" />
    </LinearLayout>

</LinearLayout>
MainActivity.kt — pełny plik
package pl.schoolit.pogoda

import android.graphics.Color
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
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 karta = findViewById<LinearLayout>(R.id.karta)
        val ivPogoda = findViewById<ImageView>(R.id.ivPogoda)
        val tvOpis = findViewById<TextView>(R.id.tvOpis)
        val tvTemperatura = findViewById<TextView>(R.id.tvTemperatura)
        val tvNumer = findViewById<TextView>(R.id.tvNumer)
        val btnPoprzedni = findViewById<Button>(R.id.btnPoprzedni)
        val btnNastepny = findViewById<Button>(R.id.btnNastepny)

        // cztery tablice tej samej długości — element nr 0 każdej opisuje słońce itd.
        val obrazki = intArrayOf(R.drawable.slonce, R.drawable.chmura, R.drawable.deszcz, R.drawable.ksiezyc)
        val opisy = arrayOf("Słonecznie", "Pochmurno", "Deszczowo", "Pogodna noc")
        val temperatury = arrayOf("24°C", "17°C", "12°C", "9°C")
        val kolory = arrayOf("#FFF8E1", "#ECEFF1", "#E3F2FD", "#E8EAF6")

        var nr = 0

        fun pokazPogode() {
            ivPogoda.setImageResource(obrazki[nr])
            ivPogoda.contentDescription = opisy[nr]
            ivPogoda.rotation = 0f                         // nowy obraz — bez obrotu
            tvOpis.text = opisy[nr]
            tvTemperatura.text = temperatury[nr]
            tvNumer.text = "${nr + 1} / ${obrazki.size}"
            karta.setBackgroundColor(Color.parseColor(kolory[nr]))
        }

        pokazPogode()

        btnNastepny.setOnClickListener {
            nr = (nr + 1) % obrazki.size                   // 0, 1, 2, 3, 0, 1…
            pokazPogode()
        }

        btnPoprzedni.setOnClickListener {
            nr = (nr - 1 + obrazki.size) % obrazki.size    // 0 → 3
            pokazPogode()
        }

        // obraz też reaguje na dotknięcie
        ivPogoda.setOnClickListener {
            ivPogoda.rotation = ivPogoda.rotation + 15f
        }
    }
}

Co dzieje się po kolei

  • Wektory mają viewportWidth/Height = 24 — współrzędne w pathData są w siatce 24 × 24. width/height = 96dp to rozmiar domyślny, ale ImageView 160 × 160 dp i tak przeskaluje obraz bez utraty ostrości.
  • W deszcz.xml element <group> zmniejsza chmurę do 80%, robiąc miejsce na krople.
  • Tablica obrazki zawiera numery zasobów (Int), dlatego jest utworzona przez intArrayOf.
  • pokazPogode() robi wszystko, co zależy od nr: obraz, opis dla czytnika ekranu, teksty, kolor karty. Przyciski zmieniają tylko nr.
  • Kolor zmienia się na karcie (LinearLayout o id karta), a nie na głównym układzie — główny ma margines, więc jego tło nie sięgałoby do krawędzi.
  • rotation to bieżący obrót w stopniach. Dotknięcie dodaje 15°, a zmiana pogody zeruje obrót.

Sprawdź trzy rzeczy

  1. Zmień w slonce.xml fillColor obu ścieżek na #E53935. Słońce zrobi się czerwone — w podglądzie pliku widać zmianę od razu.
  2. Zmień w ImageView scaleType na fitXY i szerokość na match_parent. Ikony się rozciągną. Wróć do fitCenter.
  3. Włącz na emulatorze Ustawienia → Ułatwienia dostępu → TalkBack i dotknij ikony: telefon przeczyta opis. Wyłącz TalkBack (dwukrotne przesunięcie w ustawieniach) przed dalszą pracą.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<ImageView>kontrolkaWyświetla obraz.
android:src="@drawable/…"atrybutObraz z res/drawable.
android:scaleTypeatrybutfitCenter, centerCrop, center, fitXY.
android:contentDescriptionatrybutOpis dla czytnika ekranu.
android:adjustViewBoundsatrybutDopasowanie wysokości do proporcji obrazu.
android:tintatrybutPrzemalowanie obrazu jednym kolorem.
iv.setImageResource(R.drawable.…)metodaZmienia obraz.
iv.alpha, iv.rotationFloatPrzezroczystość (0–1), obrót w stopniach.
intArrayOf(…)funkcjaTablica liczb, np. numerów zasobów.
<vector>, <path>, <group>XMLGrafika wektorowa: obraz, ścieżka, grupa z przekształceniem.
android:fillColoratrybutKolor wypełnienia ścieżki.
view.setBackgroundColor(…)metodaKolor tła dowolnej kontrolki lub układu.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
plik Moje Zdjęcie.JPG w drawableBłąd budowania: ’M’ is not a valid file-based resource name. Nazwa: małe litery, cyfry, podkreślnik.
R.drawable.slonce.xmlBłąd kompilacji. W kodzie bez rozszerzenia: R.drawable.slonce.
aplikacja zamyka się przy wyświetleniu zdjęciaCanvas: trying to draw too large bitmap — zdjęcie jest za duże. Zmniejsz je.
obraz rozciągnięty i zniekształconyObraz ustawiony jako background albo scaleType="fitXY". Użyj src i fitCenter/centerCrop.
ostrzeżenie Missing contentDescriptionDodaj opis obrazu, a dla ozdobników android:contentDescription="@null".
arrayOf(R.drawable.a, …) zamiast intArrayOfDziała, ale tworzy tablicę obiektów Int; intArrayOf jest właściwym wyborem dla liczb.
nr = nr - 1 przy cofaniuNa pierwszym obrazie nr = −1 → awaria ArrayIndexOutOfBoundsException. Użyj wzoru z resztą z dzielenia.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Cały kod pisz w onCreate. Ikony twórz przez Vector Asset → Clip art. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Moje zdjęcie★☆☆

Dodaj do projektu własne zdjęcie (np. krajobraz). Pokaż je trzy razy jedno pod drugim w kontrolkach o wysokości 150dp i szerokości ekranu, z scaleType kolejno fitCenter, centerCrop i fitXY. Pod każdym napis z nazwą trybu.

pokaż wskazówkę
  • Ten sam @drawable/… może być w wielu kontrolkach naraz.
ZAD 2Sygnalizator★☆☆

Trzy ikony koła (Vector Asset: circle) jedna pod drugą — czerwona, żółta, zielona. Przycisk Następne światło zapala kolejne: aktywne światło ma alpha = 1f, pozostałe 0.2f. Kolejność: czerwone → czerwone+żółte → zielone → żółte → czerwone…

pokaż wskazówkę
  • Kolor ikony ustawisz atrybutem android:tint — jedna ikona wystarczy dla trzech świateł. Faza świateł to liczba 0–3 zmieniana jak nr w przykładzie.
ZAD 3Galeria zabytków★★☆

Galeria pięciu zdjęć (np. zabytki twojej okolicy) z nazwą i jednozdaniowym opisem pod każdym. Przyciski ◀ i ▶, licznik „3 / 5”. Dotknięcie zdjęcia przełącza je między centerCrop a fitCenter.

pokaż wskazówkę
  • scaleType z kodu: iv.scaleType = ImageView.ScaleType.CENTER_CROP. Aktualny tryb zapamiętaj w var typu Boolean.
ZAD 4Kostka do gry z obrazkami★★☆

Narysuj sześć ścianek kostki jako wektory (kwadrat z zaokrąglonymi rogami i kropkami — możesz zmodyfikować ikonę casino albo napisać ścieżki z kół, jak tarcza słońca). Przycisk Rzuć losuje liczbę i pokazuje właściwą ściankę oraz obraca ją o losowy kąt.

pokaż wskazówkę
  • Kropka to koło: M x-r,y a r,r 0 1,0 2r,0 a r,r 0 1,0 -2r,0. Ścianki w intArrayOf, indeks to wynik rzutu minus jeden. Losowy kąt: (-20..20).random().toFloat().
ZAD 5Ubierz ludzika★★☆

Trzy ImageView jeden pod drugim: nakrycie głowy, góra, dół. Każdy ma własne przyciski ◀ ▶ po bokach, przełączające po trzy warianty (ikony z Clip art albo własne wektory). Na dole napis z opisem stroju: „czapka + bluza + spodenki”.

pokaż wskazówkę
  • Trzy osobne liczniki nr i jedna funkcja pokazStroj(). Każdy wiersz to poziomy LinearLayout: przycisk, obraz z wagą 1, przycisk.
ZAD 6Memory — pierwsza wersja★★★

Siatka 2 × 3 obrazków-kart, początkowo zakrytych (ikona help). Pod spodem są trzy pary obrazów w losowej kolejności. Dotknięcie karty ją odkrywa. Po odkryciu drugiej karty: jeśli para się zgadza — obie zostają odkryte; jeśli nie — przycisk Zakryj (aktywny tylko w tej sytuacji) zakrywa obie. Licznik ruchów i komunikat „Wygrana w 7 ruchach!”.

pokaż wskazówkę
  • Rozkład kart: val karty = mutableListOf(a, a, b, b, c, c).shuffled(). Sześć ImageView trzymaj w tablicy arrayOf(iv1, …, iv6) i podepnij im listener w pętli for (i in 0..5) — w środku i mówi, którą kartę kliknięto.
PODSUMOWANIE

Co trzeba zapamiętać

  • Obrazy trzymasz w res/drawable; nazwa pliku: małe litery, cyfry, podkreślnik.
  • Ikony twórz jako wektory (Vector Asset), zdjęcia dodawaj w rozsądnej rozdzielczości.
  • ImageView: obraz w src, dopasowanie w scaleType, opis w contentDescription.
  • Z kodu: setImageResource(R.drawable.…); listę obrazów trzymaj w intArrayOf.
  • Przechodzenie w kółko: (nr + 1) % size i (nr - 1 + size) % size.

Dokumentacja: ImageView, Vector Asset Studio, Wektory w Androidzie.

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