Android - RecyclerView spacing between items in a Grid
Asked Answered
P

5

18

In my Android app I'm using a RecyclerView to display items in a grid by using a GridLayoutManager. In a GridView, in order to specify the spacing between elements, I would set the horizontalSpacing and verticalSpacing properties.

So, how can I do the same on a RecyclerView?

Pula answered 18/7, 2015 at 13:40 Comment(3)
see this link this may help you #31243312Boudoir
But I don't want to place a divider. Should I then place an invisible view as divider?Pula
Take a look here https://mcmap.net/q/27078/-how-to-add-dividers-and-spaces-between-items-in-recyclerviewGenerality
M
17

I haven't tested it with a GridLayout but for a LinearLayout, I simply set a margin to each listitem's root layout. I guess if you want the same space between all items (let's say 8dp), you would have to do this:

  1. Set layout_padding 4 dp to the RecyclerView.
  2. Set layout_margin 4 dp to each listitem.

This way you would have a constant 8 dp around every item.


The code below is a horizontal RecyclerView that displays images with a correct spacing of 8 dp:

<!-- fragment_layout.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="4dp">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        tools:listitem="@layout/list_item" />

</LinearLayout>

<!-- list_item.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" 
    android:layout_margin="4dp">

    <ImageView
        android:layout_width="@dimen/thumbnail_size"
        android:layout_height="@dimen/thumbnail_size"
        android:contentDescription="@string/image_content_desc" />

</LinearLayout>

EDIT: I realised that the item view need to have a parent ViewGroup, so I updated the snippet.

Moro answered 30/6, 2016 at 7:36 Comment(0)
A
7

you have to use a ItemDecorator for that:

like this:

public class EqualSpaceItemDecoration extends RecyclerView.ItemDecoration {

    private final int mSpaceHeight;

    public EqualSpaceItemDecoration(int mSpaceHeight) {
        this.mSpaceHeight = mSpaceHeight;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                               RecyclerView.State state) {
        outRect.bottom = mSpaceHeight;
        outRect.top = mSpaceHeight;
        outRect.left = mSpaceHeight;
        outRect.right = mSpaceHeight;
    }
}
Ammonic answered 2/2, 2016 at 18:35 Comment(4)
You copy/paste answer not say nothing about "horizontalSpacing and verticalSpacing" :)Burnie
top+bottom=vertical, left+right+horizontal spacing , i thought it was easy enough so didn't mention it ,also its name suggest it is equalSpace means whatever you pass in it vertical and horizontal spacing will be sameAmmonic
I read this article "#24619329" and your solution not can't apply only space between 2 or more columns, this solution is do margins on a single object, not vertical or horitzontal from columns! , Note : exist a power library that not have 4 lines as your example. github.com/lucasr/twoway-viewBurnie
nice and simple, tnxLinguist
S
7

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.

enter image description here

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.

enter image description here

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
    }

}
Serology answered 24/3, 2022 at 6:52 Comment(0)
Z
2
public class EqualSpaceItemDecoration extends RecyclerView.ItemDecoration {

private final int mSpaceHeight;

public EqualSpaceItemDecoration(int mSpaceHeight) {
    this.mSpaceHeight = mSpaceHeight;
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
        RecyclerView.State state) {
    outRect.bottom = mSpaceHeight;
outRect.top = mSpaceHeight;
outRect.left = mSpaceHeight;
outRect.right = mSpaceHeight;
}

The above answer will produce unequal margin widths where the images meet.

The below solution produces even margins


 public void getItemOffsets (Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {        
        int position= parent.getChildAdapterPosition (view);
        int column = position% numberOfColumns;       
            outRect.left= margin-column*spacingPx/numberOfColumns;
            outRect.right= (column+1)* margin/numberOfColumns;
            outRect.top= margin;           

        }

    }
Zipper answered 8/2, 2018 at 12:40 Comment(3)
Just a sidenote, the positions of answers are not static.Seaweed
Please elaborateZipper
If someone upvotes your solution, it can end up being placed higher in the hierarchy so then "the above answer" makes no sense, because it assumes a specific order.Seaweed
S
2

You can use the addItemDecoration method in the recycler view.

ItemDecoration Class file:-

class ItemDecoration(space: Int) : RecyclerView.ItemDecoration() {
      private val halfSpace: Int
      override fun getItemOffsets(outRect: Rect,
                            view: View,
                            parent: RecyclerView,
                            state: RecyclerView.State) {
             if (parent.paddingLeft != halfSpace) {
                parent.setPadding(halfSpace, halfSpace, halfSpace, halfSpace)
                parent.clipToPadding = false
             }
             outRect.top = halfSpace
             outRect.bottom = halfSpace
             outRect.left = halfSpace
             outRect.right = halfSpace
    }

    init {
       halfSpace = space / 2
    }
}

Implementation in recyclerview:

rvFeeds.apply {
            layoutManager = mLM
            adapter = mAdapter
            itemAnimator = null
            addItemDecoration(ItemDecoration(resources.getDimension(R.dimen.margin_very_small).toInt())) 
}
Secant answered 5/9, 2022 at 12:46 Comment(2)
Will give +1 to this answer if both pieces of code were in Kotlin :)Moravian
Changed to Kotlin, Please check. - @MoravianSecant

© 2022 - 2024 — McMap. All rights reserved.