Кастомный ItemDecoration для RecyclerView

от автора

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

Оригинальная статья содержит 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 репозитории.

ссылка на оригинал статьи https://habr.com/ru/post/532552/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *