Предисловие переводчика:

Оригинальная статья содержит GIF-анимации с демонстрацией работы кода (т. е. результат). К сожалению, мне не удалось вставить их в статью, т. к. я использовал новый редактор от Хабра, а он очень криво работает с изображениями :) Для просмотра анимаций, вы можете перейти к оригинальной статье

Как-то раз мне нужно было создать собственный ItemDecoration, и я обнаружил, что в Интернете. почти нет ответов на этот вопрос. Надеюсь, что эта статья будет кому-нибудь полезна.

  1. Простой ItemDecoration

  2. Кастомный ItemDecoration

    1. ItemDecoration основанный на позиции

    2. ItemDecoration без отрисовки после последнего списка

    3. 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
Простой DividerItemDecoration

Преимущества

  • 4 строчки кода

  • Предоставляется самим Android

  • Прост в использовании

Недостатки

  • Как было упомянуто: ограниченные варианты использования - в качестве разделителя пустым пространством или линией.

  • Ограничение при использовании и "бесконечной" прокруткой (или пагинацией): декоратор будет отрисован для каждого элемента, в том числе и последнего. В большинстве случаев мы этого не хотим.

2. Custom ItemDecoration

Для лучшего контроля и более сложных вещей мы расширим RecyclerView.ItemDecoration.

Нам доступны 3 метода:

  • getItemOffsets используется для определения расстояния между элементами.

  • onDraw используется для отрисовки в пространстве между элементами.

  • onDrawOver то же, что и onDraw, только вызывается после того, как сам элемент отрисован.

? Cм. официальную документацию: getItemOffsets, onDraw, onDrawOver.

? Обратите внимание, что в примерах используется горизонтальная ориентация, но то же самое можно сделать и с вертикальной.

2.1. Decoration  в зависимости от позиции элемента

Для начала простой пример: давайте украсим элементы с нечетной позицией в адаптере.

Кастомный ItemDecoration на основе позиции View
Кастомный ItemDecoration на основе позиции View

Главный метод здесь здесь parent.getChildAdapterPosition (view)

Он возвращает позицию переданной View в адаптере. См. полную документацию.


?? Не путать с дочерними позициями родителей (parent.children - это элементы, отображаемые на экране).

?? Не забудьте обработать случай с RecyclerView.NO_POSITION, т. к.getChildAdapterPosition может вернуть -1.

import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView

class 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, основанный на позиции элемента в адаптере.

Пользовательское оформление DividerItemDecoration, за исключением последнего элемента
Пользовательское оформление DividerItemDecoration, за исключением последнего элемента

Исключим последний элемент, добавив if (childAdapterPosition == adapter.itemCount-1).

?? Нужно помнить, что parent.adapter может быть null.

mport android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView

class 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.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class 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, с добавлением интервала между элементами, за исключением последнего
Пользовательский DividerItemDecoration, с добавлением интервала между элементами, за исключением последнего

2.3. Оформление на основе типа View

В этом примере у нас есть 2 типа элементов, показанных черным и серым. Мы оформляем элементы синими и красными Drawable в зависимости от типа View (объявленных в Adapter-е).

Пользовательский ItemDecoration  в зависимости от типа View
Пользовательский ItemDecoration в зависимости от типа View

Главная строчка кода здесь здесь - adapter.getItemViewType(childAdapterPosition). Нам нужно переопределить метод getItemViewType в нашем адаптере.

Он возвращает тип View элемента по позиции для повторного использования View. […] Подумайте над тем, чтобы использовать id-ресурсы для уникальной идентификации типов View.

См. официальную документацию getItemViewType.

Как только вы сможете определить тип вашего элемента, вы можете установить правильный интервал и украсить его по своему желанию ?.

import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView

class 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 репозитории.