Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Created May 12, 2024 11:25
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.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ExtendedRecyclerView">
<attr name="dividerColor" format="color" />
<attr name="dividerStrokeSize" format="dimension" />
<attr name="dividerMarginStart" format="dimension" />
<attr name="dividerMarginEnd" format="dimension" />
</declare-styleable>
</resources>
import android.content.Context
import android.content.res.Resources
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
import your.package.name.R
/**
* Based on: 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
)
setDividerSizePx(dividerSize)
setDividerColor(dividerColor)
setDividerMarginStartPx(dividerMarginStart)
setDividerMarginEndPx(dividerMarginEnd)
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(): LayoutManager {
val layoutParams = layoutParams
// 0 represents matching constraints in a LinearLayout or ConstraintLayout
if (layoutParams.height == LayoutParams.MATCH_PARENT || layoutParams.height == 0) {
if (layoutParams.width == 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))
}
@Px
protected fun resToPx(@DimenRes itemSpacingRes: Int): Int {
return resources.getDimensionPixelOffset(itemSpacingRes)
}
companion object {
private fun dpToPx(dp: Float): Int {
return (dp * Resources.getSystem().displayMetrics.density).toInt()
}
}
}
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,
) : RecyclerView.ItemDecoration() {
private val dividerPaint = Paint()
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),
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 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,
@RecyclerView.Orientation
orientation: Int,
isReversed: Boolean,
) {
val isFirstPosition = position == 0
val sizeBasedOnEdge = 0
val sizeBasedOnFirstPosition = if (isFirstPosition) sizeBasedOnEdge else strokeWidthInPx
// I want to be zero, since I can get the same result using margins.
val sizeBasedOnLastPosition = 0
when (orientation) {
RecyclerView.HORIZONTAL -> {
with(outRect) {
left = if (isReversed) sizeBasedOnLastPosition else sizeBasedOnFirstPosition
top = sizeBasedOnEdge
right = if (isReversed) sizeBasedOnFirstPosition else sizeBasedOnLastPosition
bottom = sizeBasedOnEdge
}
}
RecyclerView.VERTICAL -> {
with(outRect) {
left = sizeBasedOnEdge
top = if (isReversed) sizeBasedOnLastPosition else sizeBasedOnFirstPosition
right = sizeBasedOnEdge
bottom = if (isReversed) sizeBasedOnFirstPosition else sizeBasedOnLastPosition
}
}
}
}
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 top = child.bottom
val bottom = top + strokeWidthInPx
dividerPaint.color = color
canvas.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), 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 left = child.right
val right = left + strokeWidthInPx
dividerPaint.color = color
canvas.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), dividerPaint)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment