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.
Czego się dziś nauczysz
- Dodasz do projektu zdjęcie i ikonę wektorową.
- Wyświetlisz obraz w
ImageViewz 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.
Obrazy w projekcie
Obrazy trafiają do folderu res/drawable. Android Studio obsługuje dwa rodzaje:
| Rodzaj | Co to jest | Kiedy |
|---|---|---|
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
- Skopiuj plik w Eksploratorze Windows (Ctrl+C).
- W Android Studio kliknij folder
res/drawablei wklej (Ctrl+V). Wybierz folderdrawable(bez dopisku-v24). - 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 toR.drawable.klasa_3tp.
Ikona wektorowa
- Prawy przycisk na
res→ New → Vector Asset. - Clip art → wybierz ikonę z tysięcy gotowych ikon Material (wyszukiwarka po angielsku: sunny, home, favorite…).
- Nadaj nazwę (np.
ic_dom), ustaw rozmiar i kolor → Next → Finish. Plikic_dom.xmlpojawi się wres/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).
ImageView w XML
<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" />
| Atrybut | Znaczenie |
|---|---|
android:src | Obraz do wyświetlenia: @drawable/nazwa. |
android:contentDescription | Opis obrazu czytany osobom niewidomym przez czytnik ekranu (TalkBack). Dla obrazów czysto ozdobnych: @null. |
android:scaleType | Jak dopasować obraz do rozmiaru kontrolki (niżej). |
android:adjustViewBounds="true" | Przy wrap_content wysokość dopasuje się do proporcji obrazu. |
android:tint | Przemalowuje obraz na jeden kolor — przydatne dla ikon. |
android:background | Tł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.
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:
cały obraz, puste pasy
wypełnia, boki ucięte
rozciągnięty, zniekształcony
bez skalowania, środek
| scaleType | Kiedy |
|---|---|
fitCenter | Cały obraz musi być widoczny (logo, ikona). Wartość domyślna. |
centerCrop | Zdjęcia w kafelkach i nagłówkach — ładnie wypełniają miejsce, coś z brzegów może zniknąć. |
fitXY | Prawie nigdy — obraz traci proporcje. |
center | Mały obraz pokazany w oryginalnym rozmiarze. |
Obraz z kodu
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:
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.
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.
po czwartym obrazie ▶ wraca do pierwszego; ◀ na pierwszym przechodzi do ostatniego
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Pogoda, Kotlin, API 24. - Utwórz w
res/drawablecztery pliki wektorowe: prawy przycisk nadrawable→ New → Drawable Resource File → nazwa (np.slonce) → OK, a potem zastąp całą zawartość kodem poniżej. - Podmień
activity_main.xmliMainActivity.kt.
├── drawable/
│ ├── slonce.xmlnowy
│ ├── chmura.xmlnowy
│ ├── deszcz.xmlnowy
│ └── ksiezyc.xmlnowy
└── layout/
└── activity_main.xml
<!-- 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>
<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>
<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>
<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.
<?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>
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 wpathDatasą w siatce 24 × 24.width/height = 96dpto rozmiar domyślny, aleImageView160 × 160 dp i tak przeskaluje obraz bez utraty ostrości. - W
deszcz.xmlelement<group>zmniejsza chmurę do 80%, robiąc miejsce na krople. - Tablica
obrazkizawiera numery zasobów (Int), dlatego jest utworzona przezintArrayOf. pokazPogode()robi wszystko, co zależy odnr: obraz, opis dla czytnika ekranu, teksty, kolor karty. Przyciski zmieniają tylkonr.- Kolor zmienia się na karcie (
LinearLayoutoidkarta), a nie na głównym układzie — główny ma margines, więc jego tło nie sięgałoby do krawędzi. rotationto bieżący obrót w stopniach. Dotknięcie dodaje 15°, a zmiana pogody zeruje obrót.
Sprawdź trzy rzeczy
- Zmień w
slonce.xmlfillColorobu ścieżek na#E53935. Słońce zrobi się czerwone — w podglądzie pliku widać zmianę od razu. - Zmień w
ImageViewscaleTypenafitXYi szerokość namatch_parent. Ikony się rozciągną. Wróć dofitCenter. - 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ą.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<ImageView> | kontrolka | Wyświetla obraz. |
android:src="@drawable/…" | atrybut | Obraz z res/drawable. |
android:scaleType | atrybut | fitCenter, centerCrop, center, fitXY. |
android:contentDescription | atrybut | Opis dla czytnika ekranu. |
android:adjustViewBounds | atrybut | Dopasowanie wysokości do proporcji obrazu. |
android:tint | atrybut | Przemalowanie obrazu jednym kolorem. |
iv.setImageResource(R.drawable.…) | metoda | Zmienia obraz. |
iv.alpha, iv.rotation | Float | Przezroczystość (0–1), obrót w stopniach. |
intArrayOf(…) | funkcja | Tablica liczb, np. numerów zasobów. |
<vector>, <path>, <group> | XML | Grafika wektorowa: obraz, ścieżka, grupa z przekształceniem. |
android:fillColor | atrybut | Kolor wypełnienia ścieżki. |
view.setBackgroundColor(…) | metoda | Kolor tła dowolnej kontrolki lub układu. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
plik Moje Zdjęcie.JPG w drawable | Błąd budowania: ’M’ is not a valid file-based resource name. Nazwa: małe litery, cyfry, podkreślnik. |
R.drawable.slonce.xml | Błąd kompilacji. W kodzie bez rozszerzenia: R.drawable.slonce. |
| aplikacja zamyka się przy wyświetleniu zdjęcia | Canvas: trying to draw too large bitmap — zdjęcie jest za duże. Zmniejsz je. |
| obraz rozciągnięty i zniekształcony | Obraz ustawiony jako background albo scaleType="fitXY". Użyj src i fitCenter/centerCrop. |
| ostrzeżenie Missing contentDescription | Dodaj opis obrazu, a dla ozdobników android:contentDescription="@null". |
arrayOf(R.drawable.a, …) zamiast intArrayOf | Działa, ale tworzy tablicę obiektów Int; intArrayOf jest właściwym wyborem dla liczb. |
nr = nr - 1 przy cofaniu | Na pierwszym obrazie nr = −1 → awaria ArrayIndexOutOfBoundsException. Użyj wzoru z resztą z dzielenia. |
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.
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.
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 jaknrw przykładzie.
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ę
scaleTypez kodu:iv.scaleType = ImageView.ScaleType.CENTER_CROP. Aktualny tryb zapamiętaj wvartypuBoolean.
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 wintArrayOf, indeks to wynik rzutu minus jeden. Losowy kąt:(-20..20).random().toFloat().
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
nri jedna funkcjapokazStroj(). Każdy wiersz to poziomyLinearLayout: przycisk, obraz z wagą 1, przycisk.
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śćImageViewtrzymaj w tablicyarrayOf(iv1, …, iv6)i podepnij im listener w pętlifor (i in 0..5)— w środkuimówi, którą kartę kliknięto.
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 wsrc, dopasowanie wscaleType, opis wcontentDescription.- Z kodu:
setImageResource(R.drawable.…); listę obrazów trzymaj wintArrayOf. - Przechodzenie w kółko:
(nr + 1) % sizei(nr - 1 + size) % size.
Dokumentacja: ImageView, Vector Asset Studio, Wektory w Androidzie.