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

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.

Kotlin XML ImageButton contentDescription selectableItemBackground tint BooleanArray 1 × 45 min
CEL LEKCJI

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

ImageButton w XML

przycisk z ikoną
<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ą.

TEORIA

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:backgroundEfekt
brak atrybutuSzary prostokąt — wygląda jak stary przycisk.
?attr/selectableItemBackgroundPrzezroczyste; dotyk podświetla prostokąt przycisku.
?attr/selectableItemBackgroundBorderlessPrzezroczyste; dotyk pokazuje koło wychodzące poza przycisk. Typowe dla ikon.
@nullBrak 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.

12:30
domyślneBorderlessw chwili dotyku
ta sama ikona, różne tła

trzecia ikona ma dodatkowo android:tint — przemalowanie na kolor motywu

TEORIA

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.

TEORIA

Ikona jako przełącznik

Przycisk z dwoma stanami (graj/pauza) to zmienna Boolean i if, który dobiera ikonę do stanu:

przełącznik graj / pauza
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:

tablica stanów
val ulubione = BooleanArray(4)                  // [false, false, false, false]
ulubione[2] = true                              // trzeci utwór polubiony
val ile = ulubione.count { it }                 // ile jest true → 1
PRZYKŁAD

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.

15:02
Poranek nad jeziorem
Pracownia 21 · 1 / 4
Zatrzymano · ulubione: 0
start: ikona „graj”, puste serce
→dotknięcie
„graj” i serca
15:03
Poranek nad jeziorem
Pracownia 21 · 1 / 4
Odtwarzanie · ulubione: 1
ikona „pauza”, pełne serce

przyciski poprzedni/następny mają przezroczyste tło — widać tylko ikony

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Odtwarzacz, Kotlin, API 24.
  2. Utwórz w res/drawable sześć 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.
  3. Podmień activity_main.xml i MainActivity.kt.
pokaż kod siedmiu ikon (jeśli nie używasz Vector Asset)
res/drawable/ic_graj.xml
<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>
res/drawable/ic_pauza.xml
<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>
res/drawable/ic_poprzedni.xml
<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>
res/drawable/ic_nastepny.xml
<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>
res/drawable/ic_serce_pelne.xml
<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>
res/drawable/ic_serce_puste.xml
<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>
res/drawable/ic_nuta.xml
<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.

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: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>
MainActivity.kt — pełny plik
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 ImageView z ozdobną ikoną — ma contentDescription="@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 tablica ulubione (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ówne true.

Sprawdź trzy rzeczy

  1. Polub utwory 1 i 3, przejdź po całej liście. Serce jest pełne tylko przy nich, a licznik pokazuje 2.
  2. Usuń z ibNastepny atrybut background. Pojawi się szary prostokąt — tak wygląda domyślne tło.
  3. Włącz TalkBack i dotknij środkowego przycisku przed i po kliknięciu. Telefon przeczyta raz „Odtwórz”, raz „Pauza”.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<ImageButton>kontrolkaPrzycisk z samą ikoną.
android:background="?attr/selectableItemBackgroundBorderless"atrybutPrzezroczyste tło z okrągłym efektem dotyku.
android:background="?attr/selectableItemBackground"atrybutPrzezroczyste tło z prostokątnym efektem dotyku.
android:tintatrybutKolor ikony.
android:contentDescriptionatrybutCo robi przycisk — dla czytnika ekranu.
android:tooltipTextatrybutDymek po przytrzymaniu.
ib.setImageResource(…)metodaZmienia ikonę.
ib.contentDescription = "…"właściwośćZmienia opis razem z ikoną.
!zmiennaoperatorOdwrócenie wartości logicznej.
BooleanArray(n)tablican wartości false.
tablica.count { it }IntLiczba elementów true.
if (…) "a" else "b"wyrażenieif jako wartość — wybór jednego z dwóch napisów.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
ikona zbyt duża, przycisk rozciągniętyBrak 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 contentDescriptionPrzycisk bez opisu jest niedostępny dla osób niewidomych. Opisz, co robi.
serce przełącza się dla wszystkich utworówJedna zmienna Boolean zamiast tablicy. Każdy utwór potrzebuje własnej wartości.
ikona zmienia się, opis zostaje staryZmieniaj contentDescription razem z setImageResource.
findViewById<Button>(R.id.ibGraj)Awaria ClassCastException — ImageButton nie jest Button.
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 1Głośność★☆☆

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.
ZAD 2Latarka★☆☆

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).
ZAD 3Lista zakupów z gwiazdkami★★☆

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 w BooleanArray(5), listener podpinany w pętli for (i in 0..4).
ZAD 4Ocena posiłku w stołówce★★☆

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.
ZAD 5Pilot do telewizora★★☆

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ż isEnabled wszystkich przycisków na wartość zmiennej wlaczony. Kanały w kółko: kanal = kanal % 20 + 1.
ZAD 6Zamek szyfrowy★★★

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 indeksie i zwiększa ustawione[i] = (ustawione[i] + 1) % 10.
PODSUMOWANIE

Co trzeba zapamiętać

  • ImageButton to przycisk z samą ikoną: src jak w ImageView, setOnClickListener jak w Button.
  • Ikony bez tła: android:background="?attr/selectableItemBackgroundBorderless"; kolor: android:tint.
  • Każdy przycisk z ikoną ma contentDescription mó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.

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