Предисловие переводчика:
Оригинальная статья содержит GIF-анимации с демонстрацией работы кода (т. е. результат). К сожалению, мне не удалось вставить их в статью, т. к. я использовал новый редактор от Хабра, а он очень криво работает с изображениями :) Для просмотра анимаций, вы можете перейти к оригинальной статье
Как-то раз мне нужно было создать собственный ItemDecoration, и я обнаружил, что в Интернете. почти нет ответов на этот вопрос. Надеюсь, что эта статья будет кому-нибудь полезна.
-
Простой ItemDecoration
-
Кастомный ItemDecoration
-
ItemDecoration
основанный на позиции -
ItemDecoration
без отрисовки после последнего списка -
ItemDecoration
основанный на типе View
-
1. Простой ItemDecoration : DividerItemDecoration
Основной вариант использования может быть реализован с помощью
DividerItemDecoration
предоставляемый Android-ом.
DividerItemDecoration(Context context, int orientation)
Создает разделитель RecyclerView.ItemDecorationкоторый можно использовать с LinearLayoutManager.
@param context[]будет использоваться для доступа к ресурсам.
@param orientation[]должен быть #HORIZONTAL или #VERTICAL.
Что вам нужно, так это установить правильную ориентацию, а затем предоставить drawable-ресурс, используемый для разделения каждого элемента.
recyclerView.addItemDecoration( DividerItemDecoration(context, LinearLayoutManager.HORIZONTAL) .apply { setDrawable(myDrawable) })
Простой
DividerItemDecoration
Преимущества
-
4 строчки кода
-
Предоставляется самим Android
-
Прост в использовании
Недостатки
-
Как было упомянуто: ограниченные варианты использования - в качестве разделителя пустым пространством или линией.
-
Ограничение при использовании и "бесконечной" прокруткой (или пагинацией): декоратор будет отрисован для каждого элемента, в том числе и последнего. В большинстве случаев мы этого не хотим.
2. Custom ItemDecoration
Для лучшего контроля и более сложных вещей мы расширим RecyclerView.ItemDecoration.
Нам доступны 3 метода:
-
getItemOffsets
используется для определения расстояния между элементами. -
onDraw
используется для отрисовки в пространстве между элементами. -
onDrawOver
то же, что и onDraw, только вызывается после того, как сам элемент отрисован.
? Cм. официальную документацию: getItemOffsets
,
onDraw
, onDrawOver
.
? Обратите внимание, что в примерах используется горизонтальная ориентация, но то же самое можно сделать и с вертикальной.
2.1. Decoration в зависимости от позиции элемента
Для начала простой пример: давайте украсим элементы с нечетной позицией в адаптере.
Кастомный ItemDecoration на основе позиции ViewГлавный метод здесь здесь parent.getChildAdapterPosition
(view)
Он возвращает позицию переданной View в адаптере. См. полную документацию.
Не путать с дочерними позициями родителей (parent.children
- это элементы, отображаемые на экране).Не забудьте обработать случай с
RecyclerView.NO_POSITION
, т. к.getChildAdapterPosition
может вернуть -1.
import android.graphics.Canvasimport android.graphics.Rectimport android.graphics.drawable.Drawableimport android.view.Viewimport androidx.core.view.childrenimport androidx.recyclerview.widget.RecyclerViewclass CustomPositionItemDecoration(private val dividerDrawable: Drawable) : RecyclerView.ItemDecoration() { override fun getItemOffsets(rect: Rect, view: View, parent: RecyclerView, s: RecyclerView.State) { val position = parent.getChildAdapterPosition(view) .let { if (it == RecyclerView.NO_POSITION) return else it } rect.right = if (position % 2 == 0) 2 else dividerDrawable.intrinsicWidth } override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { parent.children .forEach { view -> val position = parent.getChildAdapterPosition(view) .let { if (it == RecyclerView.NO_POSITION) return else it } if (position % 2 != 0) { val left = view.right val top = parent.paddingTop val right = left + dividerDrawable.intrinsicWidth val bottom = top + dividerDrawable.intrinsicHeight - parent.paddingBottom dividerDrawable.bounds = Rect(left, top, right, bottom) dividerDrawable.draw(canvas) } } }}
2.2. Удаляем ItemDecoration в конце списка
Это наиболее распространенный вариант использования на
StackOverflow, о котором вы спрашиваете. Это кастомный
ItemDecoration
, основанный на позиции элемента в
адаптере.
Исключим последний элемент, добавивif
(childAdapterPosition == adapter.itemCount-1)
.
Нужно помнить, чтоparent.adapter
может быть
null.
mport android.graphics.Canvasimport android.graphics.Rectimport android.graphics.drawable.Drawableimport android.view.Viewimport androidx.core.view.childrenimport androidx.recyclerview.widget.RecyclerViewclass DividerItemDecorationLastExcluded(private val dividerDrawable: Drawable) : RecyclerView.ItemDecoration() { private val dividerWidth = dividerDrawable.intrinsicWidth private val dividerHeight = dividerDrawable.intrinsicHeight override fun getItemOffsets(rect: Rect, v: View, parent: RecyclerView, s: RecyclerView.State) { parent.adapter?.let { adapter -> val childAdapterPosition = parent.getChildAdapterPosition(v) .let { if (it == RecyclerView.NO_POSITION) return else it } rect.right = // Add space/"padding" on right side if (childAdapterPosition == adapter.itemCount - 1) 0 // No "padding" else dividerWidth // Drawable width "padding" } } override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { parent.adapter?.let { adapter -> parent.children // Displayed children on screen .forEach { view -> val childAdapterPosition = parent.getChildAdapterPosition(view) .let { if (it == RecyclerView.NO_POSITION) return else it } if (childAdapterPosition != adapter.itemCount - 1) { val left = view.right val top = parent.paddingTop val right = left + dividerWidth val bottom = top + dividerHeight - parent.paddingBottom dividerDrawable.bounds = Rect(left, top, right, bottom) dividerDrawable.draw(canvas) } } } }}
? Обратите внимание, что в большинстве случаев вам, вероятно,
нужен только интервал между вашими элементами (за исключением
последнего). Нет необходимости переопределять метод
onDraw
, достаточно установить правильный размер
Rect в методе
getItemsOffsets.
import android.graphics.Rectimport android.view.Viewimport androidx.recyclerview.widget.RecyclerViewclass SimpleDividerItemDecorationLastExcluded(private val spacing: Int) : RecyclerView.ItemDecoration() { override fun getItemOffsets( rect: Rect, view: View, parent: RecyclerView, s: RecyclerView.State ) { parent.adapter?.let { adapter -> rect.right = when (parent.getChildAdapterPosition(view)) { RecyclerView.NO_POSITION, adapter.itemCount - 1 -> 0 else -> spacing } } }}
Пользовательский DividerItemDecoration, с
добавлением интервала между элементами, за исключением последнего
2.3. Оформление на основе типа View
В этом примере у нас есть 2 типа элементов, показанных черным и
серым. Мы оформляем элементы синими и красными
Drawable
в зависимости от типа View
(объявленных в Adapter-е
).
Главная строчка кода здесь здесь -
adapter.getItemViewType(childAdapterPosition)
. Нам
нужно переопределить метод getItemViewType
в нашем
адаптере.
Он возвращает тип View элемента по позиции для повторного использования View. [] Подумайте над тем, чтобы использовать id-ресурсы для уникальной идентификации типов View.
См. официальную документацию
getItemViewType
.
Как только вы сможете определить тип вашего элемента, вы можете установить правильный интервал и украсить его по своему желанию ?.
import android.content.Contextimport android.graphics.Canvasimport android.graphics.Rectimport android.graphics.drawable.Drawableimport android.view.Viewimport androidx.core.content.ContextCompatimport androidx.core.view.childrenimport androidx.recyclerview.widget.RecyclerViewclass CustomItemDecoration(context: Context) : RecyclerView.ItemDecoration() { private val decorationRed = ContextCompat.getDrawable(context, R.drawable.item_decoration_red)!! private val decorationBlue = ContextCompat.getDrawable(context, R.drawable.item_decoration_blue)!! override fun getItemOffsets(rect: Rect, view: View, parent: RecyclerView, s: RecyclerView.State) { parent.adapter?.let { adapter -> val childAdapterPosition = parent.getChildAdapterPosition(view) .let { if (it == RecyclerView.NO_POSITION) return else it } rect.right = when (adapter.getItemViewType(childAdapterPosition)) { CustomAdapter.EVEN_ITEM_ID -> decorationRed.intrinsicWidth CustomAdapter.ODD_ITEM_ID -> decorationBlue.intrinsicWidth else -> 0 } } } override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { parent.adapter?.let { adapter -> parent.children .forEach { view -> val childAdapterPosition = parent.getChildAdapterPosition(view) .let { if (it == RecyclerView.NO_POSITION) return else it } when (adapter.getItemViewType(childAdapterPosition)) { CustomAdapter.EVEN_ITEM_ID -> decorationRed.drawSeparator(view, parent, canvas) CustomAdapter.ODD_ITEM_ID -> decorationBlue.drawSeparator(view, parent, canvas) else -> Unit } } } } private fun Drawable.drawSeparator(view: View, parent: RecyclerView, canvas: Canvas) = apply { val left = view.right val top = parent.paddingTop val right = left + intrinsicWidth val bottom = top + intrinsicHeight - parent.paddingBottom bounds = Rect(left, top, right, bottom) draw(canvas) }}
Вы можете найти полный код в моём GitHub репозитории.