Galeria obrazów- kontrolka RecyclerView

Temat dotyczy tworzenia galerii zdjęć w aplikacji mobilnej, wykorzystując komponent RecyclerView. RecyclerView jest narzędziem Androida, które umożliwia wyświetlanie dużych zbiorów danych w sposób wydajny i estetyczny. Służy do pracy z dużymi i dynamicznymi listami danych. Materiał omawia, jak zbudować listę lub siatkę elementów, z których każdy zawiera obraz i podpis. Zostanie również omówiona rola adaptera, który odpowiada za łączenie danych z widokiem, oraz wykorzystanie biblioteki Coil do wygodnego ładowania grafik z zasobów lub internetu.

Coil (Coroutine Image Loader) to nowoczesna biblioteka przeznaczona dla Androida, stworzona specjalnie z myślą o języku Kotlin. Umożliwia ona szybkie i łatwe ładowanie obrazów do komponentu ImageView, zarówno z zasobów aplikacji (drawable), jak i z internetu (np. przez URL). Coil działa bardzo wydajnie, wykorzystując współbieżność opartą na Kotlin Coroutines, a przy tym jest lekka, intuicyjna i w pełni zgodna z nowoczesnym podejściem do tworzenia aplikacji. Dzięki niej możemy z łatwością wyświetlać obrazy z placeholderem.

Placeholdery (z ang. placeholder) to tymczasowe obrazy lub treści, które są wyświetlane zamiast właściwej zawartości, dopóki ta się nie załaduje.

Zapobiega to "mruganiu" lub pustym miejscom, dając użytkownikowi poczucie, że coś się dzieje (czyli UX się poprawia).

Pozwala obsłużyć błędy ładowania, a także stosować animacje przejścia (crossfade), co znacząco poprawia estetykę i wygodę korzystania z aplikacji.

Dzięki takiemu podejsciu stworzymy nowoczesny interfejs aplikacji przypominający galerię zdjęć lub katalog produktów.

Krótki opis biblioteki Coil

Coil to nowoczesna biblioteka do ładowania obrazów w aplikacjach Android, napisana specjalnie z myślą o języku Kotlin. Jej pełna nazwa to Coil ? Coroutine Image Loader.

Co robi Coil?

Coil pozwala:

Zalety Coil

Implementacja biblioteki Coil

Przejdź do pliku Gradle Scripts/ build.gradle.kts (modul app) i dodaj io.coil-kt:coil:2.6.0

Wskazówka:


dependencies {
    implementation("io.coil-kt:coil:2.6.0")
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    implementation(libs.androidx.activity)
    implementation(libs.androidx.constraintlayout)
    implementation(libs.androidx.recyclerview)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

Wykonaj synchronizację z projektem- wybierz Sync Now

W manifeście aplikacji dopiszemy uprawnienia do korzystania z Internetu

Wskazówka:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"

Layout aplikacji

W aplikacjach wykorzystujących listy lub siatki elementów, takich jak galeria zdjęć, konieczne jest przygotowanie dwóch oddzielnych layoutów. Pierwszy z nich to layout ekranu głównego (activity_main.xml), który zawiera komponent RecyclerView ? czyli kontener odpowiedzialny za wyświetlanie całej listy lub siatki elementów. Drugi to tzw. layout kafelka (item_obraz.xml), który definiuje wygląd pojedynczego elementu listy, np. obrazka z podpisem.

Dzięki takiemu podziałowi możliwe jest oddzielenie struktury całego ekranu od wyglądu pojedynczych obiektów, co pozwala na lepszą organizację kodu i ponowne wykorzystanie szablonów elementów. RecyclerView automatycznie tworzy i recyklinguje kafelki na podstawie właśnie tego drugiego layoutu ? co zapewnia zarówno elastyczność, jak i wydajność aplikacji.

Główny układ widoku

Android Studio tworzenie galerii obrazów

Zawartość pliku activity_main.xml

Wskazówka:


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
	xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:clipToPadding="false"
        android:padding="12dp"
        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
        app:spanCount="2"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Układ widoku pojedynczego elementu

Układ widoku pojedynczego elementu utworzymy przez dodanie w hierarchii projektu nowego widoku res/ layout/ element_obrazu.xml, w którym element źródła (Root) przypiszemy na

com.google.android.material.card.MaterialCardView

Patrz poniższy zrzut ekranu

Android Studio tworzenie galerii obrazów tworzenie layoutu

W widoku osadź:

Android Studio tworzenie galerii obrazów RecyclerView

Przypisane parametry widoczne są w zawartości pliku element_obraz.xml

Wskazówka:


<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="16dp"
    app:cardUseCompatPadding="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/obraz"
            android:layout_width="match_parent"
            android:layout_height="180dp"
            android:scaleType="centerCrop"
            android:adjustViewBounds="true" />

        <TextView
            android:id="@+id/tytul"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:maxLines="1"
            android:ellipsize="end"/>
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>

Model danych

Utwórz nowy pakiet dla danych. W hierarchii projektu kliknij prawym przyciskiem myszy na nazwę tworzonej aplikacji i wybierz New/ Package

Android Studio tworzenie galerii obrazów RecyclerView

Przypisz nazwę pakietowi (modelDanych). W hierarchii projektu utworzy się nowa struktura będąca folderem dla nowego pakietu. Tu utworzymy klasę danych reprezentującą pojedynczy element wczytywany adapterem do listy. Wybierz New/ Kotlin Class File a następnie Data class. Przypisz nazwę DaneObrazu

Android Studio tworzenie galerii obrazów model danych

Początkowo w pliku klasy danych będzie zgłaszany błąd, dopóki klasy danych nie wypełnimy kodem. W klasie danych zapiszemy cztery pola tak jak poniżej

Wskazówka:


package pl.afizyka.galeriazobrazw.modelDanych

import androidx.annotation.DrawableRes

data class DaneObrazu(
    //identyfikator itemu z listy
    val id: Long,
    val tytul: String,
    val url: String? = null,
    //identyfikator grafiki z zasobu
    @DrawableRes val resId: Int? = null
)

Odświeżanie elementów listy

Do przeglądania listy obrazów lub innych dużych kolekcji przygotowano specjalną klasę o nazwie DiffUtil do zastosowania w adapterach dynamicznych list. Klasa Adapter z DiffUtil odgrywa kluczową rolę w aplikacjach korzystających z komponentu RecyclerView, ponieważ odpowiada za łączenie danych z widokiem. Adapter tworzy nowe elementy widoku (ViewHoldery) i wypełnia je danymi ? w tym przypadku obrazem i podpisem. Aby lista działała wydajnie, nie powinna odświeżać się cała przy każdej zmianie danych ? i tu wkracza DiffUtil.

DiffUtil to narzędzie, które inteligentnie porównuje starą i nową listę danych, znajdując tylko te elementy, które faktycznie się zmieniły. Dzięki temu RecyclerView może np. płynnie animować dodanie, usunięcie czy zmianę pozycji bez marnowania zasobów na przerysowanie całej listy.

Połączenie ListAdapter z DiffUtil pozwala zbudować wydajną, dynamiczną i nowoczesną listę, która sprawnie reaguje na zmiany danych ? nawet w dużych zbiorach, takich jak galerie zdjęć.

Tworzymy nowa klasę w projekcie (New/ Kotlin Class File). Nadajemy jej nazwę AdapterObrazow.

Android Studio tworzenie galerii adapter obrazów

Poniżej kod

Uwaga! Zaimportuj bibliotekę coil.load. Pozostałe importy kompilator podpowie automatycznie.

Wskazówka:


package pl.afizyka.galeriazobrazw

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import pl.afizyka.galeriazobrazw.modelDanych.DaneObrazu

class AdapterObrazow (
    private val onClick: (DaneObrazu) -> Unit
) : ListAdapter<DaneObrazu, AdapterObrazow.VH>(Diff) {

    object Diff : DiffUtil.ItemCallback<DaneObrazu>() {
        override fun areItemsTheSame(old: DaneObrazu, new: DaneObrazu) = old.id == new.id
        override fun areContentsTheSame(old: DaneObrazu, new: DaneObrazu) = old == new
    }

    inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
        //odowłania do kontrolek w widoku element_obraz.xml
        private val imgView = itemView.findViewById<ImageView>(R.id.obraz)
        private val txtView = itemView.findViewById<TextView>(R.id.tytul)

        fun bind(item: DaneObrazu) {
            txtView.text = item.tytul

            val request = when {
                item.url != null -> item.url
                item.resId != null -> item.resId
                else -> null
            }

            // Coil: placeholder + błąd
            if (request != null) {
                imgView.load(request) {
                    placeholder(android.R.color.darker_gray)
                    error(android.R.color.holo_red_light)
                    crossfade(true)
                }
            } else {
                imgView.setImageResource(android.R.color.darker_gray)
            }

            itemView.setOnClickListener { onClick(item) }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        val v = LayoutInflater.from(parent.context).inflate(R.layout.element_obraz, parent, false)
        return VH(v)
    }

    override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(getItem(position))
}

Mechanizm tworzenia dynamicznej listy

W głównym pliku aplikacji MainActivity.kt zdefiniujemy dwie zmienne na kontrolkę RecyclerView i na adapter.

Wskazówka:


class MainActivity : AppCompatActivity() {
    private lateinit var recycler: RecyclerView
    private val adapter = AdapterObrazow { item ->
        Toast.makeText(this, "Klik: ${item.tytul}", Toast.LENGTH_SHORT).show()
        // Do zrobienia: przejście do ekranu pełnoekranowego jak dalsze rozwinięcie projeku
    }
    override fun onCreate(savedInstanceState: Bundle?) {

Do folderu res/ drawable skopiuj dowolne pliki graficzne, które będą dodane do listy. W celach dekoracyjnych przygotujemy prostą klasę definiującą odstępy między kafelkami i zaokrąglającą rogi.

Klasę umieść w pliku MainActivity.kt na samym końcu poza nawiasem klamrowym zamykającym zawartość pliku.

Wskazówka:


/** Prosta dekoracja odstępów między kafelkami */
class odstepyKafelkow(private val spaceDp: Int) : RecyclerView.ItemDecoration() {
    private fun Int.dpToPx(v: View) = (this * v.resources.displayMetrics.density).roundToInt()
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val px = spaceDp.dpToPx(view)
        outRect.set(px, px, px, px)
    }
}

Dodaj funkcję w klasieMainActivity.kt która w możliwie prosty sposób doda elementy do listy

Wskazówka:


private fun listaDanych(): List<DaneObrazu> = listOf(
        DaneObrazu(1, "Góry", resId = R.drawable.gory),
        DaneObrazu(2, "Las",  resId = R.drawable.las),
        DaneObrazu(3, "Morze",
		url = "https://www.epomoce.pl/ladujfotke.php?p1=Endeavour&p2=32"),
        DaneObrazu(4, "Miasto",
		url = "https://www.epomoce.pl/ladujfotke.php?p1=rw&p2=125"),
        // ...dopełnij według potrzeb
)

Zmodyfikuj metodę onCreate() do poniższej postaci

Wskazówka:


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    enableEdgeToEdge()
    setContentView(R.layout.activity_main)
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
        val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
        insets
    }
    recycler = findViewById(R.id.recycler)

    // Siatka: 2 kolumny w pionie, 3 w poziomie
    val span = if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) 2 else 3
    recycler.layoutManager = GridLayoutManager(this, span)
    recycler.setHasFixedSize(true)
    recycler.adapter = adapter
    recycler.addItemDecoration(odstepyKafelkow(12))

    adapter.submitList(listaDanych())
}

Skompiluj aplikacje i sprawdź efekt działania.

Android Studio tworzenie galerii obrazów biblioteka Coil

A aplikacja ładuje dostępne obrazy w liście. Sposób dodawania obrazów można zmodyfikować na dynamiczny. Można dodać podgląd ładowanego obrazu po kliknięciu itp.

Pełny kod pliku MainActivity.kt

Wskazówka:


package pl.afizyka.galeriazobrazw

import android.content.res.Configuration
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import pl.afizyka.galeriazobrazw.modelDanych.DaneObrazu
import kotlin.math.roundToInt

class MainActivity : AppCompatActivity() {
    private lateinit var recycler: RecyclerView
    private val adapter = AdapterObrazow { item ->
        Toast.makeText(this, "Klik: ${item.tytul}", Toast.LENGTH_SHORT).show()
        // Do zrobienia: przejście do ekranu pełnoekranowego jak dalsze rozwinięcie projektu
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        recycler = findViewById(R.id.recycler)

        // Siatka: 2 kolumny w pionie, 3 w poziomie
        val span = if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) 2 else 3
        recycler.layoutManager = GridLayoutManager(this, span)
        recycler.setHasFixedSize(true)
        recycler.adapter = adapter
        recycler.addItemDecoration(odstepyKafelkow(12))

        adapter.submitList(listaDanych())
    }

    private fun listaDanych(): List<DaneObrazu> = listOf(
        DaneObrazu(1, "Góry", resId = R.drawable.gory),
        DaneObrazu(2, "Las",  resId = R.drawable.las),
        DaneObrazu(3, "Morze",
		url = "https://www.epomoce.pl/ladujfotke.php?p1=Endeavour&p2=32"),
        DaneObrazu(4, "Miasto",
		url = "https://www.epomoce.pl/ladujfotke.php?p1=rw&p2=125"),
        // ...dopełnij według potrzeb
    )
}
/** Prosta dekoracja odstępów między kafelkami */
class odstepyKafelkow(private val spaceDp: Int) : RecyclerView.ItemDecoration() {
    private fun Int.dpToPx(v: View) = (this * v.resources.displayMetrics.density).roundToInt()
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val px = spaceDp.dpToPx(view)
        outRect.set(px, px, px, px)
    }
}

Pełny kod pliku AdapterObrazow.kt

Wskazówka:


package pl.afizyka.galeriazobrazw

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import pl.afizyka.galeriazobrazw.modelDanych.DaneObrazu

class AdapterObrazow (
    private val onClick: (DaneObrazu) -> Unit
) : ListAdapter<DaneObrazu, AdapterObrazow.VH>(Diff) {
    //obiek klasy DiffUtil usprawniającej prace z dużymi kolekcjami
    object Diff : DiffUtil.ItemCallback<DaneObrazu>() {
        override fun areItemsTheSame(old: DaneObrazu, new: DaneObrazu) = old.id == new.id
        override fun areContentsTheSame(old: DaneObrazu, new: DaneObrazu) = old == new
    }

    inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
        //odowłania do kontrolek w widoku element_obraz.xml
        private val imgView = itemView.findViewById<ImageView>(R.id.obraz)
        private val txtView = itemView.findViewById<TextView>(R.id.tytul)
        //załaduj
        fun bind(item: DaneObrazu) {
            txtView.text = item.tytul

            val request = when {
                item.url != null -> item.url
                item.resId != null -> item.resId
                else -> null
            }

            // Coil: placeholder + błąd
            if (request != null) {
                imgView.load(request) {
                    placeholder(android.R.color.darker_gray)
                    error(android.R.color.holo_red_light)
                    crossfade(true)
                }
            } else {
                imgView.setImageResource(android.R.color.darker_gray)
            }

            itemView.setOnClickListener { onClick(item) }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        val v = LayoutInflater.from(parent.context).inflate(R.layout.element_obraz, parent, false)
        return VH(v)
    }

    override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(getItem(position))
}

Pełny kod pliku DaneObrazu.kt

Wskazówka:


package pl.afizyka.galeriazobrazw.modelDanych

import androidx.annotation.DrawableRes

data class DaneObrazu(
    //identyfikator itemu z listy
    val id: Long,
    val tytul: String,
    val url: String? = null,
    //identyfikator z zasobu
    @DrawableRes val resId: Int? = null
)
Układ okresowy- kod qr
Układ okresowy

Układ okresowy pierwiastków- darmowa aplikacja na Androida

Pobierz ze sklepu Google Play
Alkomat- wirtualny test kod qr
Alkomat- wirtualny test

Alkomat- darmowa aplikacja na Androida

Pobierz ze sklepu Google Play
Światłomierz fotograficzny kod qr
Światłomierz fotograficzny

Światłomierz fotograficzny- darmowa aplikacja na Androida

Pobierz ze sklepu Google Play
Taklarz- olinowanie stałe kod qr
Olinowanie stałe- kalkulator średnic

Olinowanie stałe- darmowa aplikacja na Androida

Pobierz ze sklepu Google Play
przepis na gogfry

Przepis na gofry

zobacz
przepis na bitą śmietanę

Przepis na bitą śmietanę

zobacz