Предисловие переводчика:
Оригинальная статья содержит 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) } )
Преимущества
4 строчки кода
Предоставляется самим Android
Прост в использовании
Недостатки
Как было упомянуто: ограниченные варианты использования - в качестве разделителя пустым пространством или линией.
Ограничение при использовании и "бесконечной" прокруткой (или пагинацией): декоратор будет отрисован для каждого элемента, в том числе и последнего. В большинстве случаев мы этого не хотим.
2. Custom ItemDecoration
Для лучшего контроля и более сложных вещей мы расширим RecyclerView.ItemDecoration.
Нам доступны 3 метода:
getItemOffsetsиспользуется для определения расстояния между элементами.onDrawиспользуется для отрисовки в пространстве между элементами.onDrawOverто же, что и onDraw, только вызывается после того, как сам элемент отрисован.
? Cм. официальную документацию: getItemOffsets, onDraw, onDrawOver.
? Обратите внимание, что в примерах используется горизонтальная ориентация, но то же самое можно сделать и с вертикальной.
2.1. Меняем ItemDecoration в зависимости позиции элемента
Для начала простой пример: давайте украсим элементы с нечетной позицией в адаптере.
Главный метод здесь здесь 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, основанный на позиции элемента в адаптере.
Исключим последний элемент, добавив 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 } } } }
2.3. Оформление в зависимости от типа View
В этом примере у нас есть 2 типа элементов, показанных черным и серым. Мы оформляем элементы синими и красными Drawable в зависимости от типа View (объявленных в Adapter-е).
Главная строчка кода здесь здесь - 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 репозитории.
