Предисловие переводчика:
Оригинальная статья содержит 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. Decoration в зависимости от позиции элемента
Для начала простой пример: давайте украсим элементы с нечетной позицией в адаптере.
Главный метод здесь здесь 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 репозитории.
agent10
Ну вообще-то в интернете куча статей об ItemDecoration начиная с 16-17 года)
ImangazalievM Автор
Это слова автора оригинальной статьи) А так, да, материала много