RecyclerView
RecyclerView
- компонент для отображения элементов списка, который является более продвинутой и гибкой версией ListView
, но не является его родственником, а относится к семейству ViewGroup
.
Принцип работы
Для отображения данных RecyclerView
использует несколько компонентов:
- Объект
RecyclerView
, который нужно добавить в макет. Он заполняется элементами списка в зависимости от того, какой был установленLayoutManager
. Существуют стандартныеLayoutManager
‘ы, например,LinearLayoutManager
отображает элементы в виде списка, аGridLayoutManager
- в виде сетки. Но можно создать и свой собственныйLayoutManager
. - Элементы списка представлены в виде объектов
viewHolder
. Например, если список состоит из различных видов деревьев, тоviewHolder
- это конкретный вид дерева - сосна, яблоня, берёза и т.д.RecyclerView
создает столько объектовviewHolder
, сколько требуется для отображения на экране устройства и несколько про запас. Когда пользователь начинает прокручивать список,RecyclerView
берёт те объектыviewHolder
, которые ушли за пределы экрана и “привязывает” к ним новые данные. - Объекты
viewHolder
управляются адаптером. Он создаёт объектыviewHolder
и привязывает к ним информацию.
Добавление RecyclerView в проект
Библиотека
По умолчанию (при создании нового проекта) функциональность RecyclerView
не доступна. Поэтому для начала нужно добавить соответствующую библиотеку. Для этого в файл build.gradle
, который находится в папке app
, добавьте одну из библиотек:
1
2
3
4
5
6
7
dependencies {
// если приложение работает с библиотекой поддержки (support library)
implementation 'com.android.support:recyclerview-v7:28.0.0'
// если приложение работает с androidx
implementation 'androidx.recyclerview:recyclerview:1.1.0'
}
Версия support library уже вряд ли когда-нибудь изменится, так как её поддержку остановили. А вот за версией androidx нужно следить.
После того, как библиотека добавлена, обязательно нажмите на кнопку Sync Now, чтобы изменения вступили в силу.
Добавление в макет
Библиотека добавлена, а значит теперь мы можем обращаться к RecyclerView
. Первым делом следует добавить его в макет. Он может быть добавлен как дочерний элемент другого компонента:
1
2
3
4
5
6
7
8
9
10
11
12
// res/layout/fragment_trees_list
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
... >
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
Либо может быть единственным (корневым) компонентом макета:
1
2
3
4
5
6
7
8
9
10
// res/layout/fragment_trees_list
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
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/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Далее для корректной работы RecyclerView
требуется установить LayoutManager
и адаптер. LayoutManager
может быть установлен двумя способами. В макете:
1
2
3
4
5
6
// res/layout/fragment_trees_list
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
...
app:layoutManager="LinearLayoutManager" />
Либо вместе с адаптером в классе фрагмента или активити:
1
2
3
4
recycler_view.apply {
layoutManager = LinearLayoutManager(context)
adapter = SimpleTreesAdapter(trees)
}
При этом начиная с версии Android Studio 3.6 необязательно вызывать метод findViewById(), а можно напрямую обратится к компоненту из макета по его идентификатору (как в примере выше).
Макет элемента списка
Для элемента списка можно создать собственный макет. Например, у меня каждый элемент состоит из названия дерева и его краткого описания. Поэтому макет для элемента включает в себя два компонента TextView
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// res/layout/item_tree.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="horizontal">
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
</LinearLayout>
А можно воспользоваться стандартными макетами, к которым можно обращаться через android.R.layout.НАЗВАНИЕ_ИЗ_СПИСКА
. Но на мой взгляд это вариант для ленивых или для любопытных.
Адаптер и viewHolder
Адаптер - это класс, который занимается передачей данных в список, созданием объектов viewHolder
и их обновлением. Адаптер должен наследоваться от класса RecyclerView.Adapter
.
ViewHolder
- это тоже класс, объекты которого адаптер использует для хранения и визуализации элементов списка. ViewHolder
должен наследоваться от класса RecyclerView.ViewHolder
. Как правило этот класс располагают внутри адаптера.
1
2
3
4
5
6
7
8
9
10
11
class SimpleTreesAdapter(
private val trees: ArrayList<Tree>
) : RecyclerView.Adapter<SimpleTreesAdapter.SimpleTreesViewHolder>() {
...
class SimpleTreesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
...
}
}
В классе адаптера нужно обязательно переопределить 3 метода:
onCreateViewHolder()
- данный метод вызываетсяLayoutManager
‘ом, чтобы создать объектыviewHolder
и передать им макет, по которому будут отображаться элементы списка.onBindViewHolder()
- данный метод вызываетсяLayoutManager
‘ом, чтобы привязать к объектуviewHolder
данные, которые он должен отображать.getItemCount()
- возвращает общее количество элементов в списке.
А в классе ViewHolder
требуется указать используемые компоненты разметки.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SimpleTreesAdapter(
private val trees: ArrayList<Tree>
) : RecyclerView.Adapter<SimpleTreesAdapter.SimpleTreesViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleTreesViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_tree, parent, false)
return SimpleTreesViewHolder(view)
}
override fun onBindViewHolder(holder: SimpleTreesViewHolder, position: Int) {
val tree = trees[position]
holder.names.text = tree.name
holder.description.text = tree.description
}
override fun getItemCount(): Int = trees.size
class SimpleTreesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val names: TextView = itemView.findViewById(R.id.name)
val description: TextView = itemView.findViewById(R.id.description)
}
}
Последние штрихи
Теперь адаптер настроен и готов к использованию. Осталось только его подключить. Делается это в классе фрагмента или активити (в моём примере используется фрагмент).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class SimpleTreesFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_trees_list, container, false)
if(view is RecyclerView) {
with(view) {
layoutManager = LinearLayoutManager(context)
adapter = SimpleTreesAdapter(createData())
}
}
return view
}
// функция заполняет массив информацией, которую берёт из ресурсов, и возвращает его
private fun createData(): ArrayList<Tree> {
val names = resources.getStringArray(R.array.names)
val descriptions = resources.getStringArray(R.array.descriptions)
val trees = mutableListOf<Tree>()
for (i in names.indices) {
trees.add(Tree(name = names[i], description = descriptions[i]))
}
return trees as ArrayList<Tree>
}
// для создания фрагмента
companion object {
fun newInstance() = SimpleTreesFragment()
}
}
// класс Tree
data class Tree(
val id: String = UUID.randomUUID().toString(),
val name: String,
val description: String)
Если какой-либо элемент списка изменился, то следует вызвать метод адаптера notifyItemChanged()
и передать ему позицию элемента, которую требуется обновить. Вместо него можно использовать метод notifyDataSetChanged()
, который будет обновлять полностью весь список, но из-за этого он является ресурсозатратным.
Создание RecyclerView
с помощью шаблона
Есть возможность пропустить все вышеописанные шаги и воспользоваться стандартным шаблоном. Этот шаблон автоматически добавит в проект новый фрагмент с поддержкой RecyclerView
. Это означает, что студия за вас создаст не только фрагмент, но и адаптер, ViewHolder
, макет для элемента списка, а также код, который всё это подключает.
Кликните по любому файлу правой кнопкой мыши и в появившемся контекстном меню выберите: New > Fragment > Fragment (List).
Стандартные LayoutManager’ы
RecyclerView
использует LayoutManager
для того, чтобы расположить элементы списка на экране. При этом каждый LayoutManager
позволяет расположить их по-своему.
Виды
Существует три стандартных LayoutManager
‘а:
LinearLayoutManager
- упорядочивает элементы в виде обычного вертикального или горизонтального списка.GridLayoutManager
- размещает элементы в виде сетки одинакового размера.StaggeredGridLayoutManager
- размещает элементы в виде неравномерной сетки: каждый столбец будет слегка смещён по сравнению с предыдущим.
Как правило этих вариантов достаточно для большинства ситуаций. Но если это не ваш случай, то можно создать свой собственный LayoutManager
, расширив класс RecyclerView.LayoutManager
.
LinearLayoutManager
По умолчанию LinearLayoutManager
упорядочивает элементы в виде вертикального списка.
1
2
// вертикальный список
layoutManager = LinearLayoutManager(context)
У данного класса есть другой конструктор, который позволяет явно задать ориентацию списка. Помимо контекста, ему требуется два параметра:
- Ориентация - задаётся с помощью констант
HORIZONTAL
иVERTICAL
классаLinearLayoutManager
. - Булево значение: если передать
true
- список будет установлен в обратном порядке (начало будет в конце).
1
2
3
4
5
// вертикальная ориентация
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
// горизонтальная ориентация
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
Эти параметры можно устанавливать с помощью специальных методов:
1
2
3
4
5
val linearManager = LinearLayoutManager(context)
linearManager.apply {
orientation = LinearLayoutManager.HORIZONTAL
reverseLayout = false
}
Либо с помощью специальных атрибутов в XML:
1
2
3
4
5
<androidx.recyclerview.widget.RecyclerView
...
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="horizontal"
app:reverseLayout="false"/>
GridLayoutManager
Размещает элементы списка в виде сетки одинакового размера.
У класса GridLayoutManager
есть два конструктора. Для использования первого конструктора необходимы два параметра: контекст и количество столбцов в сетке.
1
layoutManager = GridLayoutManager(context, 3)
Для второго конструктора - четыре параметра:
- контекст;
- количество столбцов в сетке;
- ориентация списка - задаётся с помощью констант
HORIZONTAL
иVERTICAL
классаLinearLayoutManager
; - булево значение - если передать
true
- список будет установлен в обратном порядке (начало будет в конце).
Если задать горизонтальную ориентацию, то в списке будет столько рядов, сколько было задано вторым параметром (в данном примере = 3), а листаться, само собой, будет в бок.
1
layoutManager = GridLayoutManager(context, 3, LinearLayoutManager.HORIZONTAL, false)
То же самое можно задать с помощью XML атрибутов:
1
2
3
4
5
6
<androidx.recyclerview.widget.RecyclerView
...
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
android:orientation="horizontal"
app:reverseLayout="false"/>
StaggeredGridLayoutManager
Размещает элементы в виде неравномерной сетки.
У класса StaggeredGridLayoutManager
всего один конструктор с двумя параметрами:
- количество столбцов в сетке;
- ориентация списка - задаётся с помощью констант
HORIZONTAL
иVERTICAL
классаStaggeredGridLayoutManager
.
Если задать горизонтальную ориентацию, то в списке будет столько рядов, сколько было задано первым параметром (в данном примере = 3), а листаться, само собой, будет в бок.
1
layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
То же самое можно задать с помощью XML атрибутов:
1
2
3
4
5
<androidx.recyclerview.widget.RecyclerView
...
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:spanCount="3"
android:orientation="vertical" />
Динамическое переключение
Переключаться между LayoutManager
‘ами можно динамически. Например, при нажатии на кнопку:
1
2
3
4
5
6
7
8
9
10
11
12
13
btn_linear.setOnClickListener {
recycler_view.apply { layoutManager = LinearLayoutManager(requireContext()) }
}
btn_grid.setOnClickListener {
recycler_view.apply { layoutManager = GridLayoutManager(requireContext(), 3) }
}
btn_staggered.setOnClickListener {
recycler_view.apply { layoutManager =
StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
}
}
SnapHelper
SnapHelper
позволяет настроить “прилипание” элементов к определённой позиции в RecyclerView
. Например, при пролистывании можно настроить прилипание таким образом, что первый видимый элемент будет сам прилипать к краю экрана или ближайший к центру элемент будет автоматически вставать в центр экрана.
Существует два стандартных класса для работы с прилипанием элементов: LinearSnapHelper
и PagerSnapHelper
.
LinearSnapHelper
застовляет ближайший к центру элемент вставать в центр экрана. Допустим вы листаете список и в какой-то момент убрали пальцы от экрана. Список без вашего участия автоматически прокрутится и установит в центр экрана ближайший элемент.
PagerSnapHelper
предназначен для полноэкранных элементов и ведёт себя как ViewPager
.
Добавить себе в проект просто:
1
2
3
4
5
6
val snapHelper: SnapHelper = LinearSnapHelper() // или PagerSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)
// более короткий вариант
LinearSnapHelper().attachToRecyclerView(recyclerView)
PagerSnapHelper().attachToRecyclerView(recyclerView)
Если ни один вариант вас не устраивает, то создайте свою собственную реализацию этих классов и опишите в нёй необходимое поведение элементов при пролистывании списка.
Использование нескольких макетов для элементов RecyclerView
При отображении списка все его элементы выглядят одинаково и в большинстве случаев это оправдано и разработчика вполне устраивает. Тем не менее возникают ситуации, когда нужно один или целый ряд элементов выделять из остальных. Например:
- требуется добавить header или footer;
- отображение двух списков в одном
RecyclerView
; - выделение определённого элемента в списке.
ViewType
Одним из способов, который используется для выделения элементов в списке, является присвоение viewType
каждому объекту viewHolder
. ViewType
- это произвольное цифровое значение от 0 и выше, которое необходимо для того, чтобы различать объекты viewHolder
между собой. Например, если вам требуется добавить header и footer, при этом элементы списка должны выглядеть идентично, то у вас будет три viewType
: для header’а, footer’а и элемента списка.
Для начала добавьте в папку res/layout три макета: для header’а, footer’а и элемента списка.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// header.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="30sp"
android:textColor="#00695C"
android:textStyle="bold"
android:textAllCaps="true"
android:text="Header"
android:gravity="center"/>
// footer.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="30sp"
android:textColor="#C62828"
android:textStyle="bold"
android:textAllCaps="true"
android:text="Footer"
android:gravity="center"/>
// item_tree_simple.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="horizontal">
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
</LinearLayout>
В классе адаптера создадим константы, которые будут хранить значения viewType
.
1
2
3
4
5
6
7
8
9
class HeaderAndFooterAdapter {
...
companion object {
const val HEADER_VIEW = 1
const val LIST_ITEM_VIEW = 2
const val FOOTER_VIEW = 3
}
}
Для удобства создадим базовый класс GenericViewHolder
.
1
2
3
4
5
6
7
8
9
class HeaderAndFooterAdapter {
...
abstract class GenericViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bindView(position: Int)
}
}
От него будут наследоваться три класса ViewHolder
. Каждый из них отвечает за свой макет и привязку к нему данных.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private inner class ListItemViewHolder(itemView: View) : GenericViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name)
val description: TextView = itemView.findViewById(R.id.description)
override fun bindView(position: Int) {
name.text = trees[position - 1].name
description.text = trees[position - 1].description
}
}
private class HeaderViewHolder(itemView: View) : GenericViewHolder(itemView) {
val header: TextView = itemView.findViewById(R.id.header)
override fun bindView(position: Int) {
header.text = "I'm a header"
}
}
private class FooterViewHolder(itemView: View) : GenericViewHolder(itemView) {
val footer: TextView = itemView.findViewById(R.id.footer)
override fun bindView(position: Int) {
footer.text = "I'm a footer"
}
}
Обратите внимание на класс ListItemViewHolder
. В отличии от остальных он является внутренним (модификатор inner
), так как ему для привязки данных требуется обращаться к свойству trees
своего внешнего класса. Из поступившего номера позиции вычитается единица, так как нулевая позиция занята header’ом и не будет сюда поступать.
Теперь возьмёмся за код самого адаптера. С помощью метода getItemViewType()
зададим viewType
каждому объекту viewHolder
в зависимости от его позиции в списке. Первый и последний элемент списка - это header и footer. Если в списке 15 элементов, то позиция для footer’а будет 15 + 1, так как header всегда находится в нулевой позиции.
1
2
3
4
5
6
7
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> HEADER_VIEW
trees.size + 1 -> FOOTER_VIEW
else -> LIST_ITEM_VIEW
}
}
В методе onCreateViewHolder()
создаём объект viewHolder
в зависимости от viewType
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenericViewHolder {
val view: View
return when(viewType) {
HEADER_VIEW -> {
view = LayoutInflater.from(parent.context).inflate(R.layout.header, parent, false)
HeaderViewHolder(view)
}
FOOTER_VIEW -> {
view = LayoutInflater.from(parent.context).inflate(R.layout.footer, parent, false)
FooterViewHolder(view)
}
else -> {
// LIST_ITEM_VIEW
view = LayoutInflater.from(parent.context).inflate(R.layout.item_tree_simple, parent, false)
ListItemViewHolder(view)
}
}
}
В методе onBindViewHolder()
вызываем метод привязки данных bindView()
, который переопределён во всех наших классах ViewHolder
.
1
2
3
override fun onBindViewHolder(holder: GenericViewHolder, position: Int) {
holder.bindView(position)
}
Метод getItemCount()
должен возвращать количество элементов в RecyclerView
. Поэтому следует учесть наличие header’а и footer’а.
1
override fun getItemCount(): Int = trees.size + 2
Адаптер готов к использованию. Результат будет примерно таким:
Несколько списков в одном RecyclerView
У меня как-то возникала необходимость отображения двух списков на одном экране друг за другом. При этом при клике по элементу из первого списка он должен был переместиться во второй список и наоборот.
Первая появившаяся мысль - добавить на экран два RecyclerView
. И это вполне себе работает. Но возникает ряд неудобств, одно из них - некорректная работа overScroll. Эффект overScroll визуально показывает, что вы дошли до конца или начала списка.
И если на экране два RecyclerView
, то эффект overScroll появляется для каждого из них. Выглядит не очень красиво. Конечно можно overScroll эффект отключить, но тогда появляется неуверенность: “А дошел ли я до конца списка?”.
В общем нашлось решение, при котором один RecyclerView
работает с двумя списками и завязано это всё на использовании viewType
из примера выше.
Создаём два макета - один для элементов первого списка, второй - для элементов второго списка. У меня они отличаются только цветом фона - у элементов первого списка он белый, у второго - красный.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// item_tree_multiple_1.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="horizontal"
android:background="#FFFFFF">
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
</LinearLayout>
// item_tree_multiple_2.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="horizontal"
android:background="#F3BBBB">
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
</LinearLayout>
Константы для хранения значений viewType
:
1
2
3
4
companion object {
const val FIRST_LIST_ITEM_VIEW = 1
const val SECOND_LIST_ITEM_VIEW = 2
}
Также используем для удобства базовый класс GenericViewHolder
.
1
2
3
4
abstract class GenericViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bindView(position: Int)
}
От него будут наследоваться два класса ViewHolder
: один для элементов первого списка, второй для элементов второго списка.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private inner class FirstListItemViewHolder(itemView: View) : GenericViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name)
val description: TextView = itemView.findViewById(R.id.description)
override fun bindView(position: Int) {
...
}
}
private inner class SecondListItemViewHolder(itemView: View) : GenericViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name)
val description: TextView = itemView.findViewById(R.id.description)
override fun bindView(position: Int) {
...
}
}
Каждый из них будет по своему реализовывать метод bindView()
. Для элементов первого списка ничего рассчитывать не требуется, нужно просто привязать к объектам viewHolder
данные по порядку.
1
2
3
4
5
6
7
8
9
private inner class FirstListItemViewHolder(itemView: View) : GenericViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name)
val description: TextView = itemView.findViewById(R.id.description)
override fun bindView(position: Int) {
name.text = list1[position].name
description.text = list1[position].description
}
}
Второй список должен отображаться сразу после первого. Так как номер позиции может быть любым числом от 0 до list1.size + list2.size
, в методе bindView()
класса SecondListItemViewHolder
потребуется произвести расчеты.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private inner class SecondListItemViewHolder(itemView: View) : GenericViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name)
val description: TextView = itemView.findViewById(R.id.description)
override fun bindView(position: Int) {
val i = if (list1.size > 0) {
position - list1.size
} else {
position
}
name.text = list2[i].name
description.text = list2[i].description
}
}
Также обратите внимание что оба класса являются внутренними (модификатор inner
), так как им для привязки данных требуется обращаться к компонентам адаптера list1
и list2
.
Переходим к коду адаптера. В методе getItemViewType()
нужно предусмотреть все возможные сценарии: когда в обоих списках есть элементы и когда в одном из списков нет элементов.
1
2
3
4
5
6
7
8
9
10
11
override fun getItemViewType(position: Int): Int {
when {
list1.size > 0 && list2.size > 0 -> {
return if (position >= list1.size) SECOND_LIST_ITEM_VIEW
else FIRST_LIST_ITEM_VIEW
}
list1.size == 0 && list2.size > 0 -> return SECOND_LIST_ITEM_VIEW
list1.size > 0 && list2.size == 0 -> return FIRST_LIST_ITEM_VIEW
}
return super.getItemViewType(position)
}
В методе onCreateViewHolder()
создаём объект viewHolder
в зависимости от viewType
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenericViewHolder {
val view: View
return when(viewType) {
FIRST_LIST_ITEM_VIEW -> {
view = LayoutInflater.from(parent.context).inflate(R.layout.item_tree_multiple_1, parent, false)
FirstListItemViewHolder(view)
}
else -> {
// SECOND_LIST_ITEM_VIEW
view = LayoutInflater.from(parent.context).inflate(R.layout.item_tree_multiple_2, parent, false)
SecondListItemViewHolder(view)
}
}
}
В методе onBindViewHolder()
вызываем метод привязки данных bindView()
, который переопределён во всех наших классах ViewHolder
, а также вешаем слушателя.
1
2
3
4
override fun onBindViewHolder(holder: GenericViewHolder, position: Int) {
holder.bindView(position)
holder.itemView.setOnClickListener { updateUi(holder.adapterPosition, holder.itemView.context) }
}
Метод getItemCount()
должен возвращать количество элементов в RecyclerView
. Поэтому следует учесть наличие двух списков.
1
override fun getItemCount(): Int = list1.size + list2.size
При клике по элементу из первого или второго списка будет вызван метод updateUi()
, который отмечает, что по элементу кликнули и переносит его в другой список.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private fun updateUi(position: Int, context: Context) {
if (position >= list1.size) {
list2[position - list1.size].clicked = !list2[position - list1.size].clicked
} else {
list1[position].clicked = !list1[position].clicked
}
sortTrees()
Toast.makeText(context, "$position", Toast.LENGTH_SHORT).show()
}
private fun sortTrees() {
list1.clear()
list2.clear()
for (tree in trees) {
if (tree.clicked) {
list2.add(tree)
} else {
list1.add(tree)
}
}
notifyDataSetChanged()
}
Адаптер готов к использованию. Результат будет примерно таким:
Необязательно делать списки динамическими, таким образом можно отображать и статические списки. И даже комбинировать с предыдущим примером - добавлять header (один или для всех списков) и footer.
ConcatAdapter
Несмотря на то, что все примеры, описанные в предыдущем разделе, вполне себе рабочие, в плане кода выглядят не очень хорошо. В основном из-за того, что в одном адаптере скапливается множество реализаций класса ViewHolder
, а также логика их отображения. Если нам понадобится добавить или удалить какой-либо ViewHolder
, то придётся переписывать класс адаптера и заново его тестировать.
По этой причине в recyclerview:1.2.0-alpha02
был добавлен новый класс MergeAdapter
, который в версии recyclerview:1.2.0-alpha04
переименовали в ConcatAdapter
.
ConcatAdapter
позволяет отображать содержимое нескольких адаптеров в одном RecyclerView
. То есть вместо накапливания множества реализаций класса ViewHolder
в одном адаптере, мы можем создать для каждого ViewHolder
‘а свой адаптер, а потом объединить их все при помощи ConcatAdapter
. Таким образом код станет более понятным и переиспользуемым, а если потребуется добавить в RecyclerView
что-то новое - просто создадим новый адаптер.
Использование ConcatAdapter
. Обзор некоторых методов класса
Передайте в конструктор ConcatAdapter
все ваши адаптеры, которые нужно объединить, чтобы отображать их в одном RecyclerView
.
1
2
3
4
5
6
val firstAdapter = FistAdapter()
val secondAdapter = SecondAdapter()
val thirdAdapter = ThirdAdapter()
val concatAdapter = ConcatAdapter(firstAdapter, secondAdapter, thirdAdapter)
recyclerView.adapter = concatAdapter
Адаптеры будут отображаться на экране в том порядке, в котором были переданы в конструктор класса ConcatAdapter
.
Если один из адаптеров должен несколько раз отображаться на экране, то создайте несколько объектов этого адаптера и передайте их все в конструктор класса ConcatAdapter
.
1
2
3
4
5
6
7
// Первый адаптер используется дважды
val firstAdapter = FistAdapter()
val secondAdapter = SecondAdapter()
val firstAdapter = FistAdapter()
val concatAdapter = ConcatAdapter(firstAdapter, secondAdapter, firstAdapter)
recyclerView.adapter = concatAdapter
Когда мы вызываем метод notifyDataSetChanged()
в любом из адаптеров, ConcatAdapter
тоже его вызывает.
У класса ConcatAdapter
есть конструктор, который позволяет передавать список из адаптеров. На экране они будут отображаться в том порядке, в котором были добавлены в список.
1
2
3
val listOfAdapters = listOf(firstAdapter, secondAdapter, thirdAdapter)
val concatAdapter = ConcatAdapter(listOfAdapters)
recyclerView.adapter = concatAdapter
Если вам нужно добавить один из адаптеров не сразу, а позже, то используйте метод addAdapter()
. Этот метод добавляет адаптер в последнюю позицию, т.е. отображаться он будет после всех остальных.
1
2
3
val concatAdapter = ConcatAdapter(firstAdapter, secondAdapter)
...
concatAdapter.addAdapter(thirdAdapter)
Если же требуется добавить адаптер не последним, а в определённую позицию, то в метод addAdapter()
передайте номер позиции и сам адаптер. Метод добавит адаптер в указанную позицию, а все остальные адаптеры сместятся.
1
2
3
val concatAdapter = ConcatAdapter(firstAdapter, secondAdapter)
...
concatAdapter.addAdapter(0, thirdAdapter)
Обратите внимание, что номер позиции не может быть больше количества адаптеров (отсчёт начинается с нуля). В примере у нас три адаптера, каждому из которых может быть присвоена позиция 0, 1 или 2. Если указать число выше, то вылетит ошибка.
Для удаления адаптера используется метод removeAdapter()
.
1
concatAdapter.removeAdapter(firstAdapter)
Чтобы узнать сколько элементов объединил в себе ConcatAdapter
вызовите метод itemCount
. Количество элементов суммируется со всех добавленных адаптеров.
1
concatAdapter.itemCount
Можно получить список всех адаптеров, добавленных в ConcatAdapter
. Для этого вызовите adapters
, который возвращает MutableList
со всеми адаптерами.
1
concatAdapter.adapters
Обычно если в адаптере нам надо обратиться к какой-либо позиции, мы используем метод getAdapterPosition()
класса ViewHolder
. При работе с ConcatAdapter
вместо getAdapterPosition()
следует использовать getBindingAdapterPosition()
.
Пример с header’ом и footer’ом
Возьмём пример, который был в разделе ViewType: требуется отобразить header, footer и список между ними. В таком случае у нас будет три адаптера: для header’а, footer’а и элементов списка. Можно использовать и два адаптера, если логика и внешний вид header’а и footer’а идентичны. Но для наглядности в своём примере я буду использовать три.
Для начала убедитесь, что в build.gradle добавлена нужная версия библиотеки recyclerView:
1
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
Можно использовать и версию 1.2.0-alpha02
, но учтите, что в этой версии ConcatAdapter
ещё носит название MergeAdapter
.
Создадим классы данных для header’а, footer’а и элементов списка (деревья).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data class Header(
val title: String,
val color: String,
val textSize: Float
)
data class Footer(
val title: String,
val color: String,
val textSize: Float
)
data class Tree(
val name: String,
val description: String
)
Добавим макет для каждого компонента.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// header.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textAllCaps="true"
android:text="Header"
android:gravity="center"/>
// footer.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textAllCaps="true"
android:text="Footer"
android:gravity="center"/>
// item_tree_simple.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="horizontal">
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
</LinearLayout>
За отображение header’а будет отвечать HeaderAdapter
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class HeaderAdapter(
private val header: Header
) : RecyclerView.Adapter<HeaderAdapter.HeaderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val view: View = LayoutInflater.from(parent.context).inflate(R.layout.header, parent, false)
return HeaderViewHolder(view)
}
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
holder.bindView(header)
}
override fun getItemCount(): Int = 1
// ViewHolder
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val headerView: TextView = itemView.findViewById(R.id.header)
fun bindView(header: Header) {
headerView.text = header.title
headerView.setTextColor(Color.parseColor(header.color))
headerView.textSize = header.textSize
}
}
}
Для отображения элементов списка создадим ListItemAdapter
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ListItemAdapter(
private val trees: ArrayList<Tree>
) : RecyclerView.Adapter<ListItemAdapter.ListItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder {
val view: View = LayoutInflater.from(parent.context).inflate(R.layout.item_tree_simple, parent, false)
return ListItemViewHolder(view)
}
override fun onBindViewHolder(holder: ListItemViewHolder, position: Int) {
val tree = trees[position]
holder.bindView(tree)
}
override fun getItemCount(): Int = trees.size
// ViewHolder
class ListItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val name: TextView = itemView.findViewById(R.id.name)
private val description: TextView = itemView.findViewById(R.id.description)
fun bindView(tree: Tree) {
name.text = tree.name
description.text = tree.description
}
}
}
Ну и наконец адаптер для отображения footer’а.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class FooterAdapter(
private val footer: Footer
) : RecyclerView.Adapter<FooterAdapter.FooterViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FooterViewHolder {
val view: View = LayoutInflater.from(parent.context).inflate(R.layout.footer, parent, false)
return FooterViewHolder(view)
}
override fun onBindViewHolder(holder: FooterViewHolder, position: Int) {
holder.bindView(footer)
}
override fun getItemCount(): Int = 1
// ViewHolder
class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val footerView: TextView = itemView.findViewById(R.id.footer)
fun bindView(footer: Footer) {
footerView.text = footer.title
footerView.setTextColor(Color.parseColor(footer.color))
footerView.textSize = footer.textSize
}
}
}
Теперь осталось лишь объединить всё вместе в методе onCreate()
- для активити или в методе onViewCreated()
- для фрагмента. Для этого создадим по одному объекту каждого из адаптеров и передадим их классу ConcatAdapter()
в том порядке, в котором они должны быть отражены на экране.
1
2
3
4
5
6
val headerAdapter = HeaderAdapter(Header("Я - header!", "#283593", 25F))
val treesAdapter = ListItemAdapter(createData())
val footerAdapter = FooterAdapter(Footer("Я - footer!", "#6A1B9A", 25F))
val concatAdapter = ConcatAdapter(headerAdapter, treesAdapter, footerAdapter)
recycler_view.adapter = concatAdapter
Результат:
Если же в ConcatAdapter()
передать footer сразу после header’а
1
2
val concatAdapter = ConcatAdapter(headerAdapter, footerAdapter, treesAdapter)
recycler_view.adapter = concatAdapter
то результат будет таким:
Полезные ссылки
Общие ссылки по теме:
Create a List with RecyclerView - гайд из официальной документации.
RecyclerView - документация по классу (androidx).
Recyclerview - Release Notes - информация о выходе новых версий.
Using the RecyclerView - гайд от codepath.
Кастомизация:
Having multiple lists in a single RecyclerView - гайд по использованию нескольких списков в одном RecyclerView
.
Адаптеры:
ConcatAdapter - официальная документация.
Код:
RecyclerView - полный код всех примеров из данной статьи.
MergeAdapter-sample - пример реализации ConcatAdapter
от Kotlin Android Open Source.
Concat Adapter Android Example - пример реализации ConcatAdapter
от Mindorks.