Last active
April 16, 2025 10:58
-
-
Save ElianFabian/80cf13836d879e0a53c27cb7aad6d202 to your computer and use it in GitHub Desktop.
Extended RecyclerView based on: https://github.com/airbnb/epoxy/blob/master/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyRecyclerView.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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