Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

RecyclerView

Компонент RecyclerView появился в Android 5.0 Lollipop и находится в разделе Containers. Для простоты буду называть его списком, хотя на самом деле это универсальный элемент управления с большими возможностями.

Раньше для отображения прокручиваемого списка использовался ListView. Со временем у него обнаружилось множество недостатков, которые было трудно исправить. Тогда решили создать новый элемент с нуля.

Вначале это был сырой продукт, потом его доработали. На данном этапе можно считать, что он стал полноценной заменой устаревшего ListView.

Схематично работу RecyclerView можно представить следующим образом. На экране отображаются видимые элементы списка. Когда при прокрутке списка верхний элемент уходит за пределы экрана и становится невидимым, его содержимое очищается. При этом сам "чистый" элемент помещается вниз экрана и заполняется новыми данными, иными словами переиспользуется, отсюда название Recycle.

RecycleView

RecycleView

Компонент RecyclerView не является родственником ListView и относится к семейству ViewGroup. Он часто используется как замена ListView, но его возможности шире.

Следует сказать, что при работе с ним приходится писать много кода, который пугает новичков. Если с RecyclerView работать не постоянно, то порой забываются детали и сложно вспомнить необходимые шаги. Многие просто сохраняют отдельные шаблоны и копируют в разные проекты.

Внешний вид можно представить не только в виде обычного списка, но и в виде сетки. При необходимости можно быстро переключиться между разными типами отображения.

Для размещения своих дочерних элементов используется специальный менеджер макетов LayoutManager. Он может быть трёх видов.

  • LinearLayoutManager - дочерние элементы размещаются вертикально (как в ListView) или горизонтально
  • GridLayoutManager - дочерние элементы размещаются в сетке, как в GridView
  • StaggeredGridLayoutManager - неравномерная сетка

Можно создать собственный менеджер на основе RecyclerView.LayoutManager.

RecyclerView.ItemDecoration позволяет работать с дочерними элементами: отступы, внешний вид.

ItemAnimator - отвечает за анимацию элементов при добавлении, удалении и других операций.

RecyclerView.Adapter связывает данные с компонентом и отслеживает изменения.

  • notifyItemInserted(), notifyItemRemoved(), notifyItemChanged() - методы, отслеживающие добавление, удаление или изменение позиции одного элемента
  • notifyItemRangeInserted(), notifyItemRangeRemoved(), notifyItemRangeChanged() - методы, отслеживающие изменение порядка элеметов

Стандартный метод notifyDataSetChanged() поддерживается, но он не приводит к внешнему изменению элементов на экране.

Программисты со стажем знают, что для создания "правильного" ListView нужно было создавать класс ViewHolder. В старых списках его можно было игнорировать. Теперь это стало необходимым условием.

Общая модель работы компонента.

RecyclerView

Мы рассмотрим только базовый пример для быстрого знакомства. В реальных проектах примеры будут гораздо сложнее, чтобы обеспечить другие функциональные возможности - обработка жестов, анимация, динамическое удаление и добавление элементов.

Размещаем компонент в макете экрана через панель инструментов. Но сначала добавим зависимость.


implementation 'androidx.recyclerview:recyclerview:1.2.1'

Создадим макет для отдельного элемента списка. Варианты могут быть самыми разными - можно использовать один TextView для отображения строк (имена котов), можно использовать связку ImageView и TextView (имена котов и их наглые морды). Мы возьмём для примера две текстовые метки. Создадим новый файл res/layout/recyclerview_item.xml.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textViewLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <TextView
        android:id="@+id/textViewSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>

Добавим компонент в разметку экрана активности.


<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        tools:listitem="@layout/recyclerview_item"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Если вы хотите видеть макет в режиме дизайна, то добавьте в RecyclerView атрибут tools:listitem="@layout/recyclerview_item". Тогда вам будет доступен предварительный просмотр.

Существуют и другие полезные атрибуты, которые вы можете применить к компоненту. Например, вы можете добавить полосу прокрутки:


android:scrollbars="horizontal"

Минимальный код для запуска.


// в onCreate()
setContentView(R.layout.activity_main)

val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)

Пока ничего сложного, но выводить такой список ничего не будет. Нужен адаптер и данные для отображения. В адаптере описывается способ связи между данными и компонентом.

Начнём по порядку, чтобы запомнить последовательность. Для начала создадим обычный класс и в конструкторе передадим список строк. Список будет содержать имена котов.


package ru.alexanderklimov.recyclerviewdemo

class CustomRecyclerAdapter(private val names: List<String>) {

}

Класс MyViewHolder на основе ViewHolder служит для оптимизации ресурсов. Он служит своеобразным контейнером для всех компонентов, которые входят в элемент списка. При этом RecyclerView создаёт ровно столько контейнеров, сколько нужно для отображения на экране. Новый класс добавим в состав нашего созданного ранее класса. В скобках указываем название для элемента списка на основе View и этот же параметр указываем и для RecycleView.ViewHolder.


package ru.alexanderklimov.recyclerviewdemo

import android.view.View
import androidx.recyclerview.widget.RecyclerView

class CustomRecyclerAdapter(private val names: List<String>) {

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){

    }
}

В созданном классе-контейнере нужно просто перечислить используемые компоненты из макета для отдельного элемента списка. В нашем примере задействованы два TextView, инициализируем их через идентификаторы.


package ru.alexanderklimov.recyclerviewdemo

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class CustomRecyclerAdapter(private val names: List<String>) {

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val largeTextView: TextView = itemView.findViewById(R.id.textViewLarge)
        val smallTextView: TextView = itemView.findViewById(R.id.textViewSmall)
    }
}

Создадим адаптер - наследуем наш класс от класса RecyclerView.Adapter и в качестве параметра указываем созданный нами MyViewHolder. Студия попросит реализовать три метода.


class CustomRecyclerAdapter(private val names: List<String>) :
    RecyclerView.Adapter<CustomRecyclerAdapter.MyViewHolder>() {

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val largeTextView: TextView = itemView.findViewById(R.id.textViewLarge)
        val smallTextView: TextView = itemView.findViewById(R.id.textViewSmall)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        TODO("Not yet implemented")
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        TODO("Not yet implemented")
    }

    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }
}

getItemCount()

Возвращает количество элементов списка. Как правило данные являются однотипными, например, список или массив строк. Адаптеру нужно знать, сколько элементов нужно предоставить компоненту, чтобы распределить ресурсы и подготовиться к отображению на экране. При работе с коллекциями или массивом мы можем просто вычислить его длину и передать это значение методу адаптера getItemCount(). В простых случаях мы можем записать код в одну строчку.


// Длинный вариант
override fun getItemCount(): Int {
    return names.size
}

// Короткий вариант
override fun getItemCount() = names.size

onCreateViewHolder

Нам нужно указать идентификатор макета для отдельного элемента списка, созданный нами ранее в файле recyclerview_item.xml. А также вернуть наш объект класса ViewHolder. Допустим, устройство может отобразить на экране 9 элементов списка. RecyclerView создаст 11-12 элементов (с запасом). Неважно, каким большим будет ваш список, но все данные будут размещаться в тех же 11 элементах, автоматически меняя содержимое при прокрутке.


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

onBindViewHolder()

В методе адаптера onBindViewHolder() связываем используемые текстовые метки с данными - в одном случае это значения из списка, во втором используется одна и та же строка. Параметр position отвечает за позицию в списке (индекс), по которой можно получить нужные данные.


override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    holder.largeTextView.text = names[position]
    holder.smallTextView.text = "кот"
}

Должно получиться следующее.


package ru.alexanderklimov.recyclerviewdemo

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class CustomRecyclerAdapter(private val names: List<String>) : RecyclerView
.Adapter<CustomRecyclerAdapter.MyViewHolder>() {

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val largeTextView: TextView = itemView.findViewById(R.id.textViewLarge)
        val smallTextView: TextView = itemView.findViewById(R.id.textViewSmall)
    }

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

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.largeTextView.text = names[position]
        holder.smallTextView.text = "кот"
    }

    override fun getItemCount() = names.size
}

Подключаем в активности. Создадим пока бессмысленный список строк, который передадим в адаптер.


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.recyclerviewdemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = CustomRecyclerAdapter(fillList())
    }

    private fun fillList(): List<String> {
        val data = mutableListOf<String>()
        (0..30).forEach { i -> data.add("$i element") }
        return data
    }
}

Запускаем ещё раз.

RecyclerView

Вариант с числами нам не интересен, поэтому добавим котов. Имена котов и кошек разместим в ресурсах в виде массива в файле res/values/strings.xml.


<string-array name="cat_names">
    <item>Рыжик</item>
    <item>Барсик</item>
    <item>Мурзик</item>
    <item>Мурка</item>
    <item>Васька</item>
    <item>Томасина</item>
    <item>Кристина</item>
    <item>Пушок</item>
    <item>Дымка</item>
    <item>Кузя</item>
    <item>Китти</item>
    <item>Масяня</item>
    <item>Симба</item>
</string-array>

Создадим новую функцию для получения списка котов из ресурсов и передадим его адаптеру.


private fun getCatList(): List<String> {
    return this.resources.getStringArray(R.array.cat_names).toList()
}

recyclerView.adapter = CustomRecyclerAdapter(getCatList())

Горизонтальная прокрутка

Можем указать горизонтальный вариант прокрутки. Остальной код менять не придётся.


recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)

А можно вообще обойтись только XML-декларацией.


<androidx.recyclerview.widget.RecyclerView
    ...
    android:orientation="horizontal"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

Оптимизация

При прокрутке под капотом выполняется сложная работа по обновлению контента. Поэтому не следует перегружать элементы списка сложной структурой и не перебарщивайте с вложенными элементами, а также нужно следить за оптимизацией, чтобы избежать лагов.

При работе с изображениями старайтесь использовать готовые библиотеки Picasso, Glide, Fresco и т.д.

Если картинки загружаются с сервера, неплохо заранее вычислять их размеры и пропорции. В некоторых случаях желательно позаботиться, чтобы картинки были одного размера (если это возможно).

Не перегружайте лишним кодом метод onBindViewHolder(). Только самое необходимое.

Если вы используете определённое неизменяемое число элементов (число месяцев в году, знаки зодиака, число команд в турнире), то добавьте вызов метода recyclerView.setHasFixedSize(true).

В отдельных случаях можно поэкспериментировать с кэшем через recyclerView.setItemViewCacheSize(cacheSize).

Дополнительные материалы

Дополнительное чтение

Знакомство с RecyclerView (Вы здесь)

Продолжение знакомства

Менеджер макетов

Обработка щелчков, анимация

Поиск в RecyclerView

Смахиваем элементы списка в RecyclerView

RecyclerView и DiffUtil

Старый пример использования RecyclerView в выдвигающейся шторке.

sharish/ShimmerRecyclerView

Реклама