Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Last active April 16, 2025 10:58
Show Gist options
  • Save ElianFabian/80cf13836d879e0a53c27cb7aad6d202 to your computer and use it in GitHub Desktop.
Save ElianFabian/80cf13836d879e0a53c27cb7aad6d202 to your computer and use it in GitHub Desktop.
<declare-styleable name="ExtendedRecyclerView">
<attr name="dividerColor" format="color" />
<attr name="dividerStrokeSize" format="dimension" />
<attr name="dividerMarginStart" format="dimension" />
<attr name="dividerMarginEnd" format="dimension" />
<attr name="dividerMarginTop" format="dimension" />
<attr name="dividerMarginBottom" format="dimension" />
</declare-styleable>
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.Dimension
import androidx.annotation.Px
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* Base code is from: https://github.com/airbnb/epoxy/blob/master/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyRecyclerView.kt
*/
class ExtendedRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : RecyclerView(context, attrs, defStyleAttr) {
private val dividerDecorator = LineDividerItemDecoration()
init {
if (attrs != null) {
val a = context.obtainStyledAttributes(
attrs, R.styleable.ExtendedRecyclerView,
defStyleAttr, 0
)
val dividerSize = a.getDimensionPixelSize(
R.styleable.ExtendedRecyclerView_dividerStrokeSize,
0
)
val dividerColor = a.getColor(
R.styleable.ExtendedRecyclerView_dividerColor,
Color.BLACK
)
val dividerMarginStart = a.getDimensionPixelSize(
R.styleable.ExtendedRecyclerView_dividerMarginStart,
0
)
val dividerMarginEnd = a.getDimensionPixelSize(
R.styleable.ExtendedRecyclerView_dividerMarginEnd,
0
)
val dividerMarginTop = a.getDimensionPixelSize(
R.styleable.ExtendedRecyclerView_dividerMarginTop,
0
)
val dividerMarginBottom = a.getDimensionPixelSize(
R.styleable.ExtendedRecyclerView_dividerMarginBottom,
0
)
setDividerSizePx(dividerSize)
setDividerColor(dividerColor)
setDividerMarginStartPx(dividerMarginStart)
setDividerMarginEndPx(dividerMarginEnd)
setDividerMarginTopPx(dividerMarginTop)
setDividerMarginBottomPx(dividerMarginBottom)
a.recycle()
}
init()
}
private fun init() {
clipToPadding = false
}
override fun setLayoutParams(params: ViewGroup.LayoutParams) {
val isFirstParams = layoutParams == null
super.setLayoutParams(params)
if (isFirstParams) {
// Set a default layout manager if one was not set via xml
// We need layout params for this to guess at the right size and type
if (layoutManager == null) {
layoutManager = createLayoutManager()
}
}
}
/**
* Create a new [androidx.recyclerview.widget.RecyclerView.LayoutManager]
* instance to use for this RecyclerView.
*
* By default a LinearLayoutManager is used, and a reasonable default is chosen for scrolling
* direction based on layout params.
*
* If the RecyclerView is set to match parent size then the scrolling orientation is set to
* vertical and [.setHasFixedSize] is set to true.
*
* If the height is set to wrap_content then the scrolling orientation is set to horizontal, and
* [.setClipToPadding] is set to false.
*/
protected fun createLayoutManager(): RecyclerView.LayoutManager {
val layoutParams = layoutParams
// 0 represents matching constraints in a LinearLayout or ConstraintLayout
if (layoutParams.height == RecyclerView.LayoutParams.MATCH_PARENT || layoutParams.height == 0) {
if (layoutParams.width == RecyclerView.LayoutParams.MATCH_PARENT || layoutParams.width == 0) {
// If we are filling as much space as possible then we usually are fixed size
setHasFixedSize(true)
}
// A sane default is a vertically scrolling linear layout
return LinearLayoutManager(context)
} else {
// This is usually the case for horizontally scrolling carousels and should be a sane
// default
return LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
}
fun setDividerSizePx(@Px heightPx: Int) {
removeItemDecoration(dividerDecorator)
dividerDecorator.apply {
strokeWidthInPx = heightPx
}
addItemDecoration(dividerDecorator)
}
fun setDividerSizeDp(@Dimension(unit = Dimension.DP) dp: Float) {
setDividerSizePx(dpToPx(dp))
}
fun setDividerSizeRes(@DimenRes heightRes: Int) {
setDividerSizePx(resToPx(heightRes))
}
fun setDividerColor(@ColorInt color: Int) {
removeItemDecoration(dividerDecorator)
dividerDecorator.apply {
this.color = color
}
addItemDecoration(dividerDecorator)
}
fun setDividerColorRes(@ColorRes colorRes: Int) {
setDividerColor(ContextCompat.getColor(context, colorRes))
}
fun setDividerMarginStartPx(@Px marginStartPx: Int) {
removeItemDecoration(dividerDecorator)
dividerDecorator.apply {
marginStartInPx = marginStartPx
}
addItemDecoration(dividerDecorator)
}
fun setDividerMarginStartDp(@Dimension(unit = Dimension.DP) dp: Float) {
setDividerMarginStartPx(dpToPx(dp))
}
fun setDividerMarginStartRes(@DimenRes marginStartRes: Int) {
setDividerMarginStartPx(resToPx(marginStartRes))
}
fun setDividerMarginEndPx(@Px marginEndPx: Int) {
removeItemDecoration(dividerDecorator)
dividerDecorator.apply {
marginEndInPx = marginEndPx
}
addItemDecoration(dividerDecorator)
}
fun setDividerMarginEndDp(@Dimension(unit = Dimension.DP) dp: Float) {
setDividerMarginEndPx(dpToPx(dp))
}
fun setDividerMarginEndRes(@DimenRes marginEndRes: Int) {
setDividerMarginEndPx(resToPx(marginEndRes))
}
fun setDividerMarginTopPx(@Px marginTopPx: Int) {
removeItemDecoration(dividerDecorator)
dividerDecorator.apply {
marginTopInPx = marginTopPx
}
addItemDecoration(dividerDecorator)
}
fun setDividerMarginTopDp(@Dimension(unit = Dimension.DP) dp: Float) {
setDividerMarginTopPx(dpToPx(dp))
}
fun setDividerMarginTopRes(@DimenRes marginTopRes: Int) {
setDividerMarginTopPx(resToPx(marginTopRes))
}
fun setDividerMarginBottomPx(@Px marginBottomPx: Int) {
removeItemDecoration(dividerDecorator)
dividerDecorator.apply {
marginBottomInPx = marginBottomPx
}
addItemDecoration(dividerDecorator)
}
fun setDividerMarginBottomDp(@Dimension(unit = Dimension.DP) dp: Float) {
setDividerMarginBottomPx(dpToPx(dp))
}
fun setDividerMarginBottomRes(@DimenRes marginBottomRes: Int) {
setDividerMarginBottomPx(resToPx(marginBottomRes))
}
@Px
protected fun resToPx(@DimenRes itemSpacingRes: Int): Int {
return resources.getDimensionPixelOffset(itemSpacingRes)
}
}
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* Source: https://medium.com/@fadhifatah_/adaptive-item-spacing-in-recyclerview-72fb1b452232
*/
class LineDividerItemDecoration(
@ColorInt
var color: Int = Color.BLACK,
var strokeWidthInPx: Int = 0,
var marginStartInPx: Int = 0,
var marginEndInPx: Int = 0,
var marginTopInPx: Int = 0,
var marginBottomInPx: Int = 0,
) : RecyclerView.ItemDecoration() {
private val dividerPaint = Paint()
private val spacePaint = Paint().apply {
color = Color.TRANSPARENT
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State,
) {
super.getItemOffsets(outRect, view, parent, state)
val layoutManager = parent.layoutManager
if (layoutManager is GridLayoutManager) {
setGridSpacing(
outRect = outRect,
position = parent.getChildAdapterPosition(view),
itemCount = parent.adapter?.itemCount ?: 0,
orientation = layoutManager.orientation,
spanCount = layoutManager.spanCount,
isReversed = layoutManager.reverseLayout,
)
} else if (layoutManager is LinearLayoutManager) {
setLinearSpacing(
outRect = outRect,
position = parent.getChildAdapterPosition(view),
itemCount = parent.adapter?.itemCount ?: 0,
orientation = layoutManager.orientation,
isReversed = layoutManager.reverseLayout,
)
}
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val layoutManager = parent.layoutManager
if (layoutManager is GridLayoutManager) {
drawGrid(canvas, parent)
} else if (layoutManager is LinearLayoutManager) {
if (layoutManager.orientation == LinearLayoutManager.VERTICAL) {
drawVertical(canvas, parent)
} else {
drawHorizontal(canvas, parent)
}
}
}
private fun drawGrid(canvas: Canvas, parent: RecyclerView) {
val layoutManager = parent.layoutManager as GridLayoutManager
val columnCount = layoutManager.spanCount
val rowCount = layoutManager.itemCount / columnCount + 1
// Not sure how to implement divider color for grid layout
}
private fun setGridSpacing(
outRect: Rect,
position: Int,
itemCount: Int,
@RecyclerView.Orientation
orientation: Int,
spanCount: Int,
isReversed: Boolean
) {
// Basic item positioning
val isLastPosition = position == (itemCount - 1)
val sizeBasedOnLastPosition = if (isLastPosition) 0 else strokeWidthInPx
// Opposite of spanCount (find layout depth)
val subsideCount = if (itemCount % spanCount == 0) {
itemCount / spanCount
} else {
(itemCount / spanCount) + 1
}
// Grid position. Imagine all items ordered in x/y axis
val xAxis = if (orientation == RecyclerView.HORIZONTAL) position / spanCount else position % spanCount
val yAxis = if (orientation == RecyclerView.HORIZONTAL) position % spanCount else position / spanCount
// Conditions in row and column
val isFirstColumn = xAxis == 0
val isFirstRow = yAxis == 0
val isLastColumn =
if (orientation == RecyclerView.HORIZONTAL) xAxis == subsideCount - 1 else xAxis == spanCount - 1
val isLastRow =
if (orientation == RecyclerView.HORIZONTAL) yAxis == spanCount - 1 else yAxis == subsideCount - 1
// Saved size
val sizeBasedOnFirstColumn = 0
val sizeBasedOnLastColumn = if (!isLastColumn) sizeBasedOnLastPosition else 0
val sizeBasedOnFirstRow = 0
val sizeBasedOnLastRow = if (!isLastRow) strokeWidthInPx else 0
when (orientation) {
RecyclerView.HORIZONTAL -> { // Row fixed. Number of rows is spanCount
with(outRect) {
left = if (isReversed) sizeBasedOnLastColumn else sizeBasedOnFirstColumn
top = strokeWidthInPx * yAxis / spanCount
right = if (isReversed) sizeBasedOnFirstColumn else sizeBasedOnLastColumn
bottom = strokeWidthInPx * (spanCount - (yAxis + 1)) / spanCount
}
}
RecyclerView.VERTICAL -> { // Column fixed. Number of columns is spanCount
with(outRect) {
left = strokeWidthInPx * xAxis / spanCount
top = if (isReversed) sizeBasedOnLastRow else sizeBasedOnFirstRow
right = strokeWidthInPx * (spanCount - (xAxis + 1)) / spanCount
bottom = if (isReversed) sizeBasedOnFirstRow else sizeBasedOnLastRow
}
}
}
}
private fun setLinearSpacing(
outRect: Rect,
position: Int,
itemCount: Int,
@RecyclerView.Orientation orientation: Int,
isReversed: Boolean
) {
val isFirstPosition = position == 0
val isLastPosition = position == (itemCount - 1)
when (orientation) {
RecyclerView.VERTICAL -> {
with(outRect) {
top = if (isFirstPosition) 0 else marginTopInPx
bottom = if (isLastPosition) 0 else (strokeWidthInPx + marginBottomInPx)
}
}
RecyclerView.HORIZONTAL -> {
with(outRect) {
left = if (isFirstPosition) 0 else marginTopInPx
right = if (isLastPosition) 0 else (strokeWidthInPx + marginBottomInPx)
}
}
}
}
private fun drawVertical(canvas: Canvas, parent: RecyclerView) {
val childCount = parent.childCount
val left = parent.paddingLeft + marginStartInPx
val right = parent.width - parent.paddingRight - marginEndInPx
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val childBottom = child.bottom.toFloat()
val extraSpaceTop = childBottom
val extraSpaceBottom = extraSpaceTop + marginTopInPx
val dividerTop = extraSpaceBottom
val dividerBottom = dividerTop + strokeWidthInPx
canvas.drawRect(left.toFloat(), extraSpaceTop, right.toFloat(), extraSpaceBottom, spacePaint)
dividerPaint.color = color
canvas.drawRect(left.toFloat(), dividerTop, right.toFloat(), dividerBottom, dividerPaint)
}
}
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) {
val childCount = parent.childCount
val top = parent.paddingTop + marginStartInPx
val bottom = parent.height - parent.paddingBottom - marginEndInPx
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val childRight = child.right.toFloat()
val extraSpaceLeft = childRight
val extraSpaceRight = extraSpaceLeft + marginTopInPx
val dividerLeft = extraSpaceRight
val dividerRight = dividerLeft + strokeWidthInPx
canvas.drawRect(extraSpaceLeft, top.toFloat(), extraSpaceRight, bottom.toFloat(), spacePaint)
dividerPaint.color = color
canvas.drawRect(dividerLeft, top.toFloat(), dividerRight, bottom.toFloat(), dividerPaint)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment