school-it.pl // android + kotlin // rozdział 2 // lekcja 08
ImageButton — przycisk z ikoną
Odtwarzanie, pauza, polubienie, kosz, strzałka wstecz — w aplikacjach wiele przycisków nie ma napisu, tylko ikonę. Taki przycisk to ImageButton: łączy wygląd ImageView z lekcji 07 i działanie Button z lekcji 05. W tej lekcji zbudujesz panel odtwarzacza muzyki, w którym ikony zmieniają się zależnie od stanu: „graj” zamienia się w „pauzę”, a puste serce w pełne.
Czego się dziś nauczysz
- Dodasz przycisk z ikoną i obsłużysz jego kliknięcie.
- Dobierzesz tło przycisku: z ramką albo przezroczyste z efektem dotyku.
- Zapewnisz opis każdej ikony dla czytnika ekranu.
- Zrobisz z ikony przełącznik dwóch stanów (graj/pauza, serce puste/pełne).
- Zapamiętasz stan wielu elementów w tablicy
BooleanArray.
ImageButton w XML
<ImageButton
android:id="@+id/ibUsun"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@drawable/ic_usun"
android:scaleType="fitCenter"
android:padding="12dp"
android:contentDescription="Usuń wiadomość" />
Atrybuty są te same co w ImageView: src, scaleType, tint, contentDescription. W kodzie ma setOnClickListener, isEnabled i setImageResource — tak jak przycisk i obraz. padding zmniejsza ikonę wewnątrz przycisku: przycisk 56dp z paddingiem 12dp ma ikonę 32dp i duże pole do trafienia palcem.
ImageButton czy Button z ikoną?
Gdy przycisk ma ikonę i napis („🗑 Usuń”), użyj zwykłego Button z atrybutem app:icon="@drawable/ic_usun". ImageButton jest na przyciski z samą ikoną.
Tło przycisku
Domyślnie ImageButton ma szare, prostokątne tło. Ikony w paskach narzędzi i odtwarzaczach zwykle nie mają tła — widać tylko ikonę, a dotyk pokazuje się jako rozchodzące się koło (ripple). Służą do tego tła z motywu:
| android:background | Efekt |
|---|---|
| brak atrybutu | Szary prostokąt — wygląda jak stary przycisk. |
?attr/selectableItemBackground | Przezroczyste; dotyk podświetla prostokąt przycisku. |
?attr/selectableItemBackgroundBorderless | Przezroczyste; dotyk pokazuje koło wychodzące poza przycisk. Typowe dla ikon. |
@null | Brak tła i brak efektu dotyku — użytkownik nie widzi, że kliknął. Unikaj. |
Zapis ?attr/… oznacza „wartość z motywu aplikacji” — w każdym motywie efekt dotyku wygląda trochę inaczej, ale zawsze pasuje do reszty.
trzecia ikona ma dodatkowo android:tint — przemalowanie na kolor motywu
Opis ikony
Przycisk z napisem „Usuń” czytnik ekranu przeczyta sam. Przycisk z ikoną kosza — nie, bo nie wie, co przedstawia rysunek. Dlatego każdy ImageButton potrzebuje contentDescription, który mówi, co przycisk robi: „Usuń wiadomość”, a nie „ikona kosza”.
Gdy ikona się zmienia, zmienia się też opis. Przycisk pokazujący ikonę pauzy ma opis „Pauza”, a po kliknięciu — ikonę i opis „Odtwórz”. Opis pojawia się też po przytrzymaniu przycisku na nowszych Androidach, jeśli ustawisz android:tooltipText.
Ikona jako przełącznik
Przycisk z dwoma stanami (graj/pauza) to zmienna Boolean i if, który dobiera ikonę do stanu:
var gra = false
ibGraj.setOnClickListener {
gra = !gra // odwrócenie: true ↔ false
if (gra) {
ibGraj.setImageResource(R.drawable.ic_pauza)
ibGraj.contentDescription = "Pauza"
} else {
ibGraj.setImageResource(R.drawable.ic_graj)
ibGraj.contentDescription = "Odtwórz"
}
}
Gdy stan dotyczy wielu elementów (każdy utwór może być ulubiony), potrzebujesz wielu wartości Boolean — tablicy BooleanArray:
val ulubione = BooleanArray(4) // [false, false, false, false]
ulubione[2] = true // trzeci utwór polubiony
val ile = ulubione.count { it } // ile jest true → 1
Panel odtwarzacza
Odtwarzacz z listą czterech utworów (bez prawdziwego dźwięku — ćwiczymy przyciski). Ikony poprzedni i następny przełączają utwór, duży przycisk w środku przełącza graj/pauza, a serce dodaje bieżący utwór do ulubionych. Każdy utwór pamięta, czy jest ulubiony — po powrocie do niego serce jest pełne.
„graj” i serca
przyciski poprzedni/następny mają przezroczyste tło — widać tylko ikony
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Odtwarzacz, Kotlin, API 24. - Utwórz w
res/drawablesześć ikon. Najprościej przez Vector Asset → Clip art: play arrow →ic_graj, pause →ic_pauza, skip previous →ic_poprzedni, skip next →ic_nastepny, favorite →ic_serce_pelne, favorite border →ic_serce_puste, music note →ic_nuta. Możesz też utworzyć pliki ręcznie z kodu poniżej. - Podmień
activity_main.xmliMainActivity.kt.
pokaż kod siedmiu ikon (jeśli nie używasz Vector Asset)
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000" android:pathData="M8,5v14l11,-7z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000" android:pathData="M6,19h4V5H6v14zm8,-14v14h4V5h-4z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000" android:pathData="M6,6h2v12H6zm3.5,6l8.5,6V6z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000" android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000"
android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000"
android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z" />
</vector>
Ikony są czarne — kolor nadaje im w układzie atrybut android:tint. Dzięki temu ta sama ikona może mieć różne kolory w różnych miejscach.
<?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:gravity="center_horizontal"
android:orientation="vertical"
android:layout_margin="16dp"
android:fitsSystemWindows="true">
<!-- okładka: ozdobny obraz, bez opisu -->
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="16dp"
android:background="#E8DEF8"
android:contentDescription="@null"
android:padding="48dp"
android:src="@drawable/ic_nuta"
android:tint="#6750A4" />
<TextView
android:id="@+id/tvTytul"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvWykonawca"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp" />
<!-- trzy przyciski sterowania w wierszu -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageButton
android:id="@+id/ibPoprzedni"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Poprzedni utwór"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_poprzedni" />
<ImageButton
android:id="@+id/ibGraj"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Odtwórz"
android:scaleType="fitCenter"
android:src="@drawable/ic_graj"
android:tint="#6750A4" />
<ImageButton
android:id="@+id/ibNastepny"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Następny utwór"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_nastepny" />
</LinearLayout>
<ImageButton
android:id="@+id/ibUlubiony"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Dodaj do ulubionych"
android:padding="10dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_serce_puste"
android:tint="#C62828" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp" />
</LinearLayout>
package pl.schoolit.odtwarzacz
import android.os.Bundle
import android.widget.ImageButton
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 tvTytul = findViewById<TextView>(R.id.tvTytul)
val tvWykonawca = findViewById<TextView>(R.id.tvWykonawca)
val tvStatus = findViewById<TextView>(R.id.tvStatus)
val ibPoprzedni = findViewById<ImageButton>(R.id.ibPoprzedni)
val ibGraj = findViewById<ImageButton>(R.id.ibGraj)
val ibNastepny = findViewById<ImageButton>(R.id.ibNastepny)
val ibUlubiony = findViewById<ImageButton>(R.id.ibUlubiony)
val tytuly = arrayOf("Poranek nad jeziorem", "Szkolny korytarz", "Ostatni dzwonek", "Wakacje w drodze")
val wykonawcy = arrayOf("Pracownia 21", "Chór ZSMI", "Pracownia 21", "Duet Kod i Klawisz")
val ulubione = BooleanArray(tytuly.size) // wszystkie na start false
var nr = 0
var gra = false
fun pokazOdtwarzacz() {
tvTytul.text = tytuly[nr]
tvWykonawca.text = "${wykonawcy[nr]} · ${nr + 1} / ${tytuly.size}"
// ikona i opis środkowego przycisku zależą od stanu gra
if (gra) {
ibGraj.setImageResource(R.drawable.ic_pauza)
ibGraj.contentDescription = "Pauza"
} else {
ibGraj.setImageResource(R.drawable.ic_graj)
ibGraj.contentDescription = "Odtwórz"
}
// serce zależy od tego, czy BIEŻĄCY utwór jest ulubiony
if (ulubione[nr]) {
ibUlubiony.setImageResource(R.drawable.ic_serce_pelne)
ibUlubiony.contentDescription = "Usuń z ulubionych"
} else {
ibUlubiony.setImageResource(R.drawable.ic_serce_puste)
ibUlubiony.contentDescription = "Dodaj do ulubionych"
}
val stan = if (gra) "Odtwarzanie" else "Zatrzymano"
tvStatus.text = "$stan · ulubione: ${ulubione.count { it }}"
}
pokazOdtwarzacz()
ibGraj.setOnClickListener {
gra = !gra
pokazOdtwarzacz()
}
ibNastepny.setOnClickListener {
nr = (nr + 1) % tytuly.size
pokazOdtwarzacz()
}
ibPoprzedni.setOnClickListener {
nr = (nr - 1 + tytuly.size) % tytuly.size
pokazOdtwarzacz()
}
ibUlubiony.setOnClickListener {
ulubione[nr] = !ulubione[nr] // przełącz tylko bieżący utwór
pokazOdtwarzacz()
}
}
}
Co dzieje się po kolei
- Okładka to zwykły
ImageViewz ozdobną ikoną — macontentDescription="@null", bo czytnik nie powinien jej czytać. - Przyciski poprzedni/następny/serce mają przezroczyste tło z efektem koła. Padding zmniejsza ikonę, ale cały przycisk 56dp reaguje na dotyk.
- Stan aplikacji to trzy rzeczy:
nr(który utwór),gra(graj/pauza) i tablicaulubione(po jednej wartości na utwór). pokazOdtwarzacz()ustawia wszystko według stanu: teksty, ikonę i opis środkowego przycisku, ikonę i opis serca, status. Przyciski tylko zmieniają stan i wołają tę funkcję.ulubione[nr] = !ulubione[nr]przełącza serce tylko dla bieżącego utworu. Po przejściu do innego utworu i powrocie serce pokazuje zapamiętany stan.ulubione.count { it }liczy elementy równetrue.
Sprawdź trzy rzeczy
- Polub utwory 1 i 3, przejdź po całej liście. Serce jest pełne tylko przy nich, a licznik pokazuje 2.
- Usuń z
ibNastepnyatrybutbackground. Pojawi się szary prostokąt — tak wygląda domyślne tło. - Włącz TalkBack i dotknij środkowego przycisku przed i po kliknięciu. Telefon przeczyta raz „Odtwórz”, raz „Pauza”.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<ImageButton> | kontrolka | Przycisk z samą ikoną. |
android:background="?attr/selectableItemBackgroundBorderless" | atrybut | Przezroczyste tło z okrągłym efektem dotyku. |
android:background="?attr/selectableItemBackground" | atrybut | Przezroczyste tło z prostokątnym efektem dotyku. |
android:tint | atrybut | Kolor ikony. |
android:contentDescription | atrybut | Co robi przycisk — dla czytnika ekranu. |
android:tooltipText | atrybut | Dymek po przytrzymaniu. |
ib.setImageResource(…) | metoda | Zmienia ikonę. |
ib.contentDescription = "…" | właściwość | Zmienia opis razem z ikoną. |
!zmienna | operator | Odwrócenie wartości logicznej. |
BooleanArray(n) | tablica | n wartości false. |
tablica.count { it } | Int | Liczba elementów true. |
if (…) "a" else "b" | wyrażenie | if jako wartość — wybór jednego z dwóch napisów. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| ikona zbyt duża, przycisk rozciągnięty | Brak stałego rozmiaru albo scaleType. Ustaw np. 56dp × 56dp, fitCenter i padding. |
| szary prostokąt pod ikoną | Domyślne tło. Ustaw ?attr/selectableItemBackgroundBorderless. |
android:background="@null" | Brak efektu dotyku — użytkownik nie wie, czy kliknął. |
| ostrzeżenie Missing contentDescription | Przycisk bez opisu jest niedostępny dla osób niewidomych. Opisz, co robi. |
| serce przełącza się dla wszystkich utworów | Jedna zmienna Boolean zamiast tablicy. Każdy utwór potrzebuje własnej wartości. |
| ikona zmienia się, opis zostaje stary | Zmieniaj contentDescription razem z setImageResource. |
findViewById<Button>(R.id.ibGraj) | Awaria ClassCastException — ImageButton nie jest Button. |
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.
Dwa przyciski z ikonami volume down i volume up i napis „Głośność: 5” między nimi (zakres 0–10). Przy 0 ikona przycisku ciszej się wyłącza, przy 10 — głośniej. Trzeci przycisk z ikoną volume off wycisza (0) i przy następnym kliknięciu przywraca poprzednią głośność.
pokaż wskazówkę
- Przed wyciszeniem zapamiętaj głośność w drugiej zmiennej
var przedWyciszeniem.
Duży przycisk z ikoną flashlight off. Kliknięcie zmienia ikonę na flashlight on, tło ekranu na białe, a opis przycisku na „Wyłącz latarkę”. Kolejne kliknięcie przywraca ciemny ekran.
pokaż wskazówkę
- Kolor ikony przy ciemnym tle zmień z kodu:
ib.setColorFilter(Color.WHITE).
Pięć produktów w pięciu wierszach. Każdy wiersz: nazwa produktu i przycisk z ikoną star border. Kliknięcie gwiazdki zmienia ją na pełną (star, kolor żółty) — produkt jest „ważny”. Na dole: „Ważne: chleb, mleko”.
pokaż wskazówkę
- Pięć przycisków w tablicy
arrayOf(ib1, ib2, …), stany wBooleanArray(5), listener podpinany w pętlifor (i in 0..4).
Trzy przyciski z buźkami: sentiment dissatisfied, sentiment neutral, sentiment satisfied. Kliknięta buźka robi się kolorowa (czerwona, żółta, zielona), pozostałe szare. Pod spodem liczniki głosów dla każdej buźki i przycisk z ikoną refresh, który zeruje głosy.
pokaż wskazówkę
- Kolor ikony z kodu:
ib.setColorFilter(Color.parseColor("#…")); szara:Color.GRAY.
Pilot: przycisk zasilania, kanał w górę/w dół (strzałki), głośność +/−, wyciszenie. Ekran „telewizora” to TextView z czarnym tłem pokazujący „Kanał 3 · Głośność 12”. Przy wyłączonym telewizorze ekran jest pusty, a wszystkie przyciski poza zasilaniem są wyłączone. Kanały 1–20 w kółko.
pokaż wskazówkę
- Funkcja
pokazTelewizor()ustawia teżisEnabledwszystkich przycisków na wartość zmiennejwlaczony. Kanały w kółko:kanal = kanal % 20 + 1.
Kłódka (ikona lock) i cztery „kółka” z cyframi 0–9; nad i pod każdą cyfrą przyciski ze strzałkami keyboard arrow up/down. Szyfr jest losowany przy starcie. Gdy ustawione cyfry zgadzają się z szyfrem, ikona kłódki zmienia się na lock open i robi się zielona, a strzałki się wyłączają. Przycisk z ikoną help pokazuje, ile cyfr jest na dobrym miejscu (bez wskazywania których).
pokaż wskazówkę
- Szyfr i ustawienie jako
IntArray(4). Osiem przycisków strzałek podepnij w pętli: przycisk w górę o indeksieizwiększaustawione[i] = (ustawione[i] + 1) % 10.
Co trzeba zapamiętać
ImageButtonto przycisk z samą ikoną:srcjak wImageView,setOnClickListenerjak wButton.- Ikony bez tła:
android:background="?attr/selectableItemBackgroundBorderless"; kolor:android:tint. - Każdy przycisk z ikoną ma
contentDescriptionmówiący, co robi — i zmienia go razem z ikoną. - Przełącznik dwóch stanów to
Boolean+!; stany wielu elementów —BooleanArray. - Wszystko, co zależy od stanu, ustawiaj w jednej funkcji wywoływanej po każdej zmianie.
Dokumentacja: ImageButton, Opisywanie elementów dla czytników ekranu.