Created
May 12, 2024 11:25
-
-
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 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
<?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> |
This file contains 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.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() | |
} | |
} | |
} |
This file contains 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, | |
) : 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