In RecyclerView
, spacing between items could be added using ItemDecoration
.
Before that let's do some maths to find out margin around list items.
Here we get equally spaced areas. In each area, we have to find their margins and return it in the outRect
. To be equally spaced and equally sized, if you do the maths correctly, you will find that these offset values are not constants, they get changed based on their column index and the row index. Let's derive equations for the offset values.
With Outer Edge
In the first case, we consider margin around the edges.
For 0 and 1,
d + w + a = d − a + w + c
c = 2a
For 0 and 2,
d + w + a = d − c + w + e
e = a + c
e = 3a
If you see the pattern here,
right = a, 2a, 3a, ... , (n + 1) × a
right = (nᵢ + 1) × a
na = d
a = d ÷ n
right = (nᵢ + 1) × d ÷ n
Pattern of left side,
left = d, d − a, d − 2a, d − 3a, ... , d − na
left = d − nᵢ × a
left = d − nᵢ × d ÷ n
nᵢ - Column index
Without Outer Edge
Here we do our maths without considering edges.
For 0 and 1,
w + a = d − a + w + c
c = 2a − d
For 0 and 2,
w + a = d − c + w + e
e = 3a − 2d
The pattern here is,
right = a, 2a − d, 3a − 2d, ... , (n + 1) × a - nd
right = (nᵢ + 1) × a - nᵢd
(n + 1) × a − nd = 0
a = nd ÷ (n + 1)
∴ right = d − d × (nᵢ + 1) ÷ q
q = n + 1
q - Column count
nᵢ - Column index
left = 0, d − a, 2(d − a), ... , n(d − a)
left = nᵢ(d − a)
left = nᵢ × d ÷ q
Let's write code for this...
class SpacingItemDecoration(
private val spacing: Int,
private val includeEdge: Boolean,
private val headerRowCount: Int = 0
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val columnCount = getColumnCount(parent)
val position = parent.getChildAdapterPosition(view) - headerRowCount * columnCount
if (position >= 0) {
val rowCount = getRowCount(parent, columnCount)
val orientation = getOrientation(parent)
val columnIndex = position % columnCount
val rowIndex = position / columnCount
if (includeEdge) {
when (orientation) {
RecyclerView.VERTICAL -> {
outRect.left = getStartOffsetWithEdge(spacing, columnIndex, columnCount)
outRect.right = getEndOffsetWithEdge(spacing, columnIndex, columnCount)
outRect.top = getStartOffsetWithEdge(spacing, rowIndex, rowCount)
outRect.bottom = getEndOffsetWithEdge(spacing, rowIndex, rowCount)
}
RecyclerView.HORIZONTAL -> {
outRect.top = getStartOffsetWithEdge(spacing, columnIndex, columnCount)
outRect.bottom = getEndOffsetWithEdge(spacing, columnIndex, columnCount)
outRect.left = getStartOffsetWithEdge(spacing, rowIndex, rowCount)
outRect.right = getEndOffsetWithEdge(spacing, rowIndex, rowCount)
}
}
} else {
when (orientation) {
RecyclerView.VERTICAL -> {
outRect.left = getStartOffsetWithoutEdge(spacing, columnIndex, columnCount)
outRect.right = getEndOffsetWithoutEdge(spacing, columnIndex, columnCount)
outRect.top = getStartOffsetWithoutEdge(spacing, rowIndex, rowCount)
outRect.bottom = getEndOffsetWithoutEdge(spacing, rowIndex, rowCount)
}
RecyclerView.HORIZONTAL -> {
outRect.top = getStartOffsetWithoutEdge(spacing, columnIndex, columnCount)
outRect.bottom = getEndOffsetWithoutEdge(spacing, columnIndex, columnCount)
outRect.left = getStartOffsetWithoutEdge(spacing, rowIndex, rowCount)
outRect.right = getEndOffsetWithoutEdge(spacing, rowIndex, rowCount)
}
}
}
} else {
outRect.left = 0
outRect.right = 0
outRect.top = 0
outRect.bottom = 0
}
}
private fun getColumnCount(parent: RecyclerView) = when (val layoutManager = parent.layoutManager) {
is GridLayoutManager -> layoutManager.spanCount
is StaggeredGridLayoutManager -> layoutManager.spanCount
else -> 1
}
private fun getRowCount(parent: RecyclerView, columnCount: Int) =
parent.adapter?.itemCount?.div(columnCount)?.minus(headerRowCount)
private fun getOrientation(parent: RecyclerView) = when (val layoutManager = parent.layoutManager) {
is LinearLayoutManager -> layoutManager.orientation
is GridLayoutManager -> layoutManager.orientation
is StaggeredGridLayoutManager -> layoutManager.orientation
else -> RecyclerView.VERTICAL
}
private fun getStartOffsetWithEdge(spacing: Int, columnIndex: Int, columnCount: Int?): Int {
if (columnCount == null) return spacing
return spacing - spacing * columnIndex / columnCount
}
private fun getEndOffsetWithEdge(spacing: Int, columnIndex: Int, columnCount: Int?): Int {
if (columnCount == null) return 0
return spacing * (columnIndex + 1) / columnCount
}
private fun getStartOffsetWithoutEdge(spacing: Int, columnIndex: Int, columnCount: Int?): Int {
if (columnCount == null) return 0
return spacing * columnIndex / columnCount
}
private fun getEndOffsetWithoutEdge(spacing: Int, columnIndex: Int, columnCount: Int?): Int {
if (columnCount == null) return spacing
return spacing - spacing * (columnIndex + 1) / columnCount
}
}