Ernestina Juan
Ernestina Juan

Reputation: 967

Android - RecyclerView spacing between items in a Grid

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?

Upvotes: 18

Views: 53341

Answers (5)

Thakar Sahil
Thakar Sahil

Reputation: 269

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())) 
}

Upvotes: 3

UdaraWanasinghe
UdaraWanasinghe

Reputation: 2852

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
    }

}

Upvotes: 7

JAAD
JAAD

Reputation: 12379

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;
    }
}

Upvotes: 7

Thejashwini Dev
Thejashwini Dev

Reputation: 331

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;           

        }

    }

Upvotes: 2

josemigallas
josemigallas

Reputation: 3909

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.

Upvotes: 17

Related Questions