Android Recyclerview GridLayoutManager column spacing

291,688

Solution 1

RecyclerViews support the concept of ItemDecoration: special offsets and drawing around each element. As seen in this answer, you can use

public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
  private int space;

  public SpacesItemDecoration(int space) {
    this.space = space;
  }

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

    // Add top margin only for the first item to avoid double space between items
    if (parent.getChildLayoutPosition(view) == 0) {
        outRect.top = space;
    } else {
        outRect.top = 0;
    }
  }
}

Then add it via

mRecyclerView = (RecyclerView) rootView.findViewById(R.id.my_recycler_view);
int spacingInPixels = getResources().getDimensionPixelSize(R.dimen.spacing);
mRecyclerView.addItemDecoration(new SpacesItemDecoration(spacingInPixels));

Solution 2

Following code works well, and each column has same width:

public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {

    private int spanCount;
    private int spacing;
    private boolean includeEdge;

    public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
        this.spanCount = spanCount;
        this.spacing = spacing;
        this.includeEdge = includeEdge;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view); // item position
        int column = position % spanCount; // item column

        if (includeEdge) {
            outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
            outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)

            if (position < spanCount) { // top edge
                outRect.top = spacing;
            }
            outRect.bottom = spacing; // item bottom
        } else {
            outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
            outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
            if (position >= spanCount) {
                outRect.top = spacing; // item top
            }
        }
    }
}

Usage

1. no edge

enter image description here

int spanCount = 3; // 3 columns
int spacing = 50; // 50px
boolean includeEdge = false;
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));

2. with edge

enter image description here

int spanCount = 3; // 3 columns
int spacing = 50; // 50px
boolean includeEdge = true;
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));

Solution 3

The following is the step-by-step simple solution if you want the equal spacing around items and equal item sizes.

ItemOffsetDecoration

public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {

    private int mItemOffset;

    public ItemOffsetDecoration(int itemOffset) {
        mItemOffset = itemOffset;
    }

    public ItemOffsetDecoration(@NonNull Context context, @DimenRes int itemOffsetId) {
        this(context.getResources().getDimensionPixelSize(itemOffsetId));
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset);
    }
}

Implementation

In your source code, add ItemOffsetDecoration to your RecyclerView. Item offset value should be half size of the actual value you want to add as space between items.

mRecyclerView.setLayoutManager(new GridLayoutManager(context, NUM_COLUMNS);
ItemOffsetDecoration itemDecoration = new ItemOffsetDecoration(context, R.dimen.item_offset);
mRecyclerView.addItemDecoration(itemDecoration);

Also, set item offset value as padding for itsRecyclerView, and specify android:clipToPadding=false.

<android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerview_grid"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:padding="@dimen/item_offset"/>

Solution 4

Try this. It'll take care of equal spacing all around. Works both with List, Grid, and StaggeredGrid.

Edited

The updated code should handle most of the corner cases with spans, orientation, etc. Note that if using setSpanSizeLookup() with GridLayoutManager, setting setSpanIndexCacheEnabled() is recommended for performance reasons.

Note, it seems that with StaggeredGrid, there's seems to be a bug where the index of the children gets wacky and hard to track so the code below might not work very well with StaggeredGridLayoutManager.

public class ListSpacingDecoration extends RecyclerView.ItemDecoration {

  private static final int VERTICAL = OrientationHelper.VERTICAL;

  private int orientation = -1;
  private int spanCount = -1;
  private int spacing;
  private int halfSpacing;


  public ListSpacingDecoration(Context context, @DimenRes int spacingDimen) {

    spacing = context.getResources().getDimensionPixelSize(spacingDimen);
    halfSpacing = spacing / 2;
  }

  public ListSpacingDecoration(int spacingPx) {

    spacing = spacingPx;
    halfSpacing = spacing / 2;
  }

  @Override
  public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

    super.getItemOffsets(outRect, view, parent, state);

    if (orientation == -1) {
        orientation = getOrientation(parent);
    }

    if (spanCount == -1) {
        spanCount = getTotalSpan(parent);
    }

    int childCount = parent.getLayoutManager().getItemCount();
    int childIndex = parent.getChildAdapterPosition(view);

    int itemSpanSize = getItemSpanSize(parent, childIndex);
    int spanIndex = getItemSpanIndex(parent, childIndex);

    /* INVALID SPAN */
    if (spanCount < 1) return;

    setSpacings(outRect, parent, childCount, childIndex, itemSpanSize, spanIndex);
  }

  protected void setSpacings(Rect outRect, RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    outRect.top = halfSpacing;
    outRect.bottom = halfSpacing;
    outRect.left = halfSpacing;
    outRect.right = halfSpacing;

    if (isTopEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
        outRect.top = spacing;
    }

    if (isLeftEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
        outRect.left = spacing;
    }

    if (isRightEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
        outRect.right = spacing;
    }

    if (isBottomEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
        outRect.bottom = spacing;
    }
  }

  @SuppressWarnings("all")
  protected int getTotalSpan(RecyclerView parent) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getSpanCount();
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return ((StaggeredGridLayoutManager) mgr).getSpanCount();
    } else if (mgr instanceof LinearLayoutManager) {
        return 1;
    }

    return -1;
  }

  @SuppressWarnings("all")
  protected int getItemSpanSize(RecyclerView parent, int childIndex) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanSize(childIndex);
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return 1;
    } else if (mgr instanceof LinearLayoutManager) {
        return 1;
    }

    return -1;
  }

  @SuppressWarnings("all")
  protected int getItemSpanIndex(RecyclerView parent, int childIndex) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanIndex(childIndex, spanCount);
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return childIndex % spanCount;
    } else if (mgr instanceof LinearLayoutManager) {
        return 0;
    }

    return -1;
  }

  @SuppressWarnings("all")
  protected int getOrientation(RecyclerView parent) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof LinearLayoutManager) {
        return ((LinearLayoutManager) mgr).getOrientation();
    } else if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getOrientation();
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return ((StaggeredGridLayoutManager) mgr).getOrientation();
    }

    return VERTICAL;
  }

  protected boolean isLeftEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    if (orientation == VERTICAL) {

        return spanIndex == 0;

    } else {

        return (childIndex == 0) || isFirstItemEdgeValid((childIndex < spanCount), parent, childIndex);
    }
  }

  protected boolean isRightEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    if (orientation == VERTICAL) {

        return (spanIndex + itemSpanSize) == spanCount;

    } else {

        return isLastItemEdgeValid((childIndex >= childCount - spanCount), parent, childCount, childIndex, spanIndex);
    }
  }

  protected boolean isTopEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    if (orientation == VERTICAL) {

        return (childIndex == 0) || isFirstItemEdgeValid((childIndex < spanCount), parent, childIndex);

    } else {

        return spanIndex == 0;
    }
  }

  protected boolean isBottomEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    if (orientation == VERTICAL) {

        return isLastItemEdgeValid((childIndex >= childCount - spanCount), parent, childCount, childIndex, spanIndex);

    } else {

        return (spanIndex + itemSpanSize) == spanCount;
    }
  }

  protected boolean isFirstItemEdgeValid(boolean isOneOfFirstItems, RecyclerView parent, int childIndex) {

    int totalSpanArea = 0;
    if (isOneOfFirstItems) {
        for (int i = childIndex; i >= 0; i--) {
            totalSpanArea = totalSpanArea + getItemSpanSize(parent, i);
        }
    }

    return isOneOfFirstItems && totalSpanArea <= spanCount;
  }

  protected boolean isLastItemEdgeValid(boolean isOneOfLastItems, RecyclerView parent, int childCount, int childIndex, int spanIndex) {

    int totalSpanRemaining = 0;
    if (isOneOfLastItems) {
        for (int i = childIndex; i < childCount; i++) {
            totalSpanRemaining = totalSpanRemaining + getItemSpanSize(parent, i);
        }
    }

    return isOneOfLastItems && (totalSpanRemaining <= spanCount - spanIndex);
  }
}

Hope it helps.

Solution 5

There is only one easy solution, that you can remember and implement wherever needed. No bugs, no crazy calculations. Put margin to the card / item layout and put the same size as padding to the RecyclerView:

item_layout.xml

<CardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:margin="10dp">

activity_layout.xml

<RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"/>

UPDATE: enter image description here

Share:
291,688

Related videos on Youtube

Nick H
Author by

Nick H

KC3SPW

Updated on May 09, 2021

Comments

  • Nick H
    Nick H about 3 years

    How do you set the column spacing with a RecyclerView using a GridLayoutManager? Setting the margin/padding inside my layout has no effect.

  • Amio.io
    Amio.io about 9 years
    Use 'outRect.top = space' and remove 'outRect.bottom' if you don't want to mess with the 'if for the first position'. ;-]
  • ianhanniballake
    ianhanniballake about 9 years
    @zatziky - yep, if you already use top and bottom padding as part of your RecyclerView (and use clipToPadding="false"), then you can restructure things slightly. If you don't however, you'd just be moving the if check to be the last time (as you'd still want the bottom padding on the last item).
  • IvanP
    IvanP about 9 years
    I've got double span just after first line of items. It happens because parent.getChildCount() returns 1 for first item, 2 for second and so on. So, I suggest add space to items of the top edge like: outRect.top = childIndex < spanCount ? spacingInPixels : 0; And add bottom space for each item: outRect.bottom = spacingInPixels;
  • Yugesh
    Yugesh about 9 years
    At the time of scrolling RecyclerView, spacing changed.
  • yqritc
    yqritc about 9 years
    I think parent.getChildCount() should be changed to "parent.getLayoutManager().getItemCount()". Also, isBottomEdge function need to be changed to "return childIndex >= childCount - spanCount + spanIndex". After changing these, I got equal spacing. But please note that this solution does not give me equal item sizes if span count is greater than 2 since offset value is different depending on position.
  • bkurzius
    bkurzius almost 9 years
    I tried this and unless I added android:clipToPadding="false" and android:padding="@dimen/spacing" to the RecyclerView the outside spacing did not match the spacing between items. I have tried all the answers below and think the solution by @yqritc was the cleanest and simplest.
  • Matthew
    Matthew almost 9 years
    Works unless you have items that have various spans, like headers.
  • Avinash R
    Avinash R almost 9 years
    @ianhanniballake, while this works when using a single span layout manager, it fails for multi-span layout manager.
  • Korniltsev Anatoly
    Korniltsev Anatoly almost 9 years
    It worth to mention, that you have to modify all fields of outRect. otherwise you can get the offset of the previous view.
  • Samir
    Samir almost 9 years
    This is the most simple one. One important thing is you also got to add the padding to the parent in the xml. In my case, it work that way. Thanks.
  • Mark Hetherington
    Mark Hetherington almost 9 years
    The SpaceItemDecoration actually adds the padding to the parent (the recycler view).
  • Samir
    Samir almost 9 years
    only halfSpace padding appeared(to the right side) when I had not set the padding to the parent in xml
  • Mark Hetherington
    Mark Hetherington almost 9 years
    It was only missing on the right side? It may be that you have half space set as the leftPadding on the left side already in the xml and this code only checks if the left padding is set on the RecyclerView or not.
  • Samir
    Samir almost 9 years
    Well I don't have any padding set in the xml.
  • Pirdad Sakhizada
    Pirdad Sakhizada over 8 years
    @yqritc thanks for nocticing parent.getChildCount(). I've updated my answer to use the parent.getLayoutManager().getItemCount()
  • Pirdad Sakhizada
    Pirdad Sakhizada over 8 years
    @IvanP I've update the code to handle the issue you're facing. Let me know if you're still seeing it.
  • Yaroslav
    Yaroslav over 8 years
    If you do it this way with GridLayoutManager - all first items of the 2nd, 3rd...nth column will stick to top (because there is no space). So I think it is better to do .top = space / 2 and .bottom = space / 2.
  • Kevin Lee
    Kevin Lee over 8 years
    Great answer; one tip: spacing is in px (so you can convert dp to px using Math.round(someDpValue * getResources().getDisplayMetrics().density))
  • Haris Qurashi
    Haris Qurashi over 8 years
    Its working fine but i am having a problem, i am using GridLayoutManager with spanCount of 2(default) but user can change the spanCount so when spanCount changes from default position there is a much more visible padding on some positions like if spanCount will 3 than padding/margin on 2,3 8,9 12,13 etc.
  • Ufkoku
    Ufkoku over 8 years
    Works great! But I have some problems with StaggeredGridLayoutManager. imgur.com/XVutH5u horizontal margins sometimes differs.
  • Admin
    Admin about 8 years
    How you can apply only bettween column and bottom ? [img]spacebetween[img] , for example I solve this problem in gridview normal with " android:horizontalSpacing="2dp" android:verticalSpacing="2dp"" in recycle I don't know how
  • Veaceslav Gaidarji
    Veaceslav Gaidarji about 8 years
    @HarisQureshi I had same problem. You have to remove item decoration every time you change spanCount.
  • Meanman
    Meanman about 8 years
    Works great, thank you, however there is a problem with odd number of columns. The right hand column does not align with the edge of the screen, it has one or two pixels of padding. So I take the padding, reset it to zero, then I distribute to the middle column. This is done by dividing the distributable amount by two, adding to the left/right and adding any remainder to the right. For this to work I pre-calculate all of the spacings once in my constructor (instead of each time the list scrolls) and store them in an array of left/right values. Hope this helps
  • Sergio Serra
    Sergio Serra almost 8 years
    You should use StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); int position = lp.getSpanIndex(); to get the position instead of parent.getChildAdapterPosition(view); this will solve the issues with bad calculations.
  • Xingxing
    Xingxing over 7 years
  • Vulovic Vukasin
    Vulovic Vukasin over 7 years
    Best solution there is, thank you. If you have problems with first item in recycler view, from the code you put in for GridSpacingItemDecoration, remove last if(position >= spanCount){outRect.top = spacing;} and you are good to go. Many thanks to @edwardaa
  • hmac
    hmac over 7 years
    getChildAdapterPosition (instead of getChildLayoutPosition) is better in most cases, I was getting problems refreshing itemDecorations after views had switched positions - the new layout hasn't finished while the getItemOffsets is called....
  • Mahdi
    Mahdi over 7 years
    I have question in your answer. I have the same situation that can not make Images center. cause I have many sizes images that load in ImageView and I put attribute with a fixed size in dp and CropCenter but it not actually center. why it happends?
  • Ajeet
    Ajeet over 7 years
    Hi. This works amazing but I am using a header with your solution. Can you suggest how can achieve full-width header?
  • Praneeth
    Praneeth over 7 years
    what is mMargin you are passing in? what value should i pass
  • Mark Hetherington
    Mark Hetherington over 7 years
    @praneethkumar that number is spacing in pixels you want
  • Masoud Mohammadi
    Masoud Mohammadi almost 7 years
    This doesn't work when layout is rtl (for 2 columns or more). currently the space between columns is not right when in rtl mode. you need to replace: outRect.left with outRect.right when it is in rtl.
  • Pierre-Luc Paour
    Pierre-Luc Paour over 6 years
    This worked very well out of the box even with variable spans, congratulations and thank you!
  • Konstantin Konopko
    Konstantin Konopko over 6 years
    Do not forget set grid item root layout layout_width="match_parent"
  • John Lee
    John Lee over 6 years
    @Matthew Including Header is much easier than you think, As you can see, in "getItemOffsets", it's setting it's own position. So what you would do is, make ItemDecoration with boolean value "withHeader", and in "getItemOffsets", >> if( withHeader) { if( position == 0 ) return; else position--; }
  • Sunil
    Sunil over 6 years
    will it work according to screen size means the way is it showing on the 5-inch screen, they look same on other screen sizes also?
  • Linh
    Linh over 6 years
    the size of item will fixed but the space between item may different, you can see too 2 images above for understand
  • Sunil
    Sunil over 6 years
    They look different on different screen sizes.any way but working thank you
  • Sahil Patel
    Sahil Patel about 6 years
    If you have a toggle switch that toggles between list to grid, don't forget to call recyclerView.removeItemDecoration() before setting any new Item decoration. If not then the new calculations for the spacing would be incorrect.
  • Tim Kranen
    Tim Kranen about 6 years
    What is headerNum?
  • salmanseifian
    salmanseifian almost 6 years
    this parent.getChildLayoutPosition(view) == 0 puts second item in first row a little lower. I think it should be edited
  • Kuva
    Kuva over 5 years
    This decision has a problem: items can be not the same width. It depends on space and column count. So i've made some changes in this answer: stackoverflow.com/a/52538671/6251577
  • SDJSK
    SDJSK over 5 years
    excellent calculation for the left & right. perfect same spacing
  • Shirane85
    Shirane85 over 5 years
    Great answer man! Works for all cases, including not symmetrical "regular" GridLayoutManager where you have an header item between items. Thanks!
  • B.shruti
    B.shruti about 5 years
    Perfect, it's simple and effective.
  • Avi
    Avi about 5 years
    This is the more generic answer and should have been the marked one
  • hch
    hch about 5 years
    This answer does not answer the original question. The emphasis of question is on GridLayoutManager . The answer will not work on multi-column/row layouts
  • elyar abad
    elyar abad over 4 years
    It works fine! could you elaborate on the issue, please?
  • elyar abad
    elyar abad over 4 years
    Thank you so much! I was looking for some technical reason why such a cooperation between recycler's padding and item's margin is needed. Any way you did so much for me . . .
  • Mina Samir
    Mina Samir over 4 years
    kindly you can check the position with layout manager as the following layoutManager.getPosition(view) after that check if the position is zero that will be your header .. also, this way will enable you to add another header at any positions you want :)
  • user924
    user924 over 4 years
    you didn't consider span count
  • zeleven
    zeleven about 4 years
    It worked, but I can't understand the calculation, can someone explain in more detail? Why can't I set left for all items except the last one and set right for only last one?
  • massivemadness
    massivemadness about 4 years
    I would add following lines, if someone like me using GridLayoutManager with spanCount=1 columnCount == 1 -> { outRect.left = space outRect.right = space }
  • Amir Ziarati
    Amir Ziarati almost 4 years
    only first item from the first row will have top margin. the second wont. change the if to: if (parent.getChildLayoutPosition(view) < columnsCount) and take columnsCount as argument. or use edwardaa answer.
  • DIRTY DAVE
    DIRTY DAVE over 3 years
    @Matthew Have you found another solution for multiple spans? This one works fine but as soon as I have .setSpanSizeLookup Some of the spacing is messed up
  • Dharmishtha
    Dharmishtha over 3 years
    perfect solution!
  • Mahdi Safarmohammadloo
    Mahdi Safarmohammadloo over 3 years
    between space is 2x ! its problem !
  • Veena
    Veena about 3 years
    @Pirdad Sakhizada The solution works nice for Portrait, but if I change the orientation of phone to landscape it messes the spacing. Can you please suggest me where I am going wrong.
  • Xam
    Xam almost 3 years
    Interesting library, I'll give it a try.
  • Xam
    Xam almost 3 years
    Well, I tried your library. More specifically, I adapted your code to make it work only with the VERTICAL orientation for both linear and grid layout managers and it works great.
  • Alireza Farahani
    Alireza Farahani almost 3 years
    @Xam Glad you liked it.
  • Naveen Kumawat
    Naveen Kumawat over 2 years
    Bro, you're genius _/.
  • LXJ
    LXJ over 2 years
    for some reason I have to set outRect.bottom and outRect.top to -20 to get to some acceptable row spacing.
  • CoolMind
    CoolMind over 2 years
    What is it? A simple vertical RV without grid?
  • Remc4
    Remc4 over 2 years
    Add parenthesis around (spacing / spanCount) for more stable integer division rounding. Without them I had first column 161px wide, second and third 160px and the last one 159px.
  • Kilnn
    Kilnn about 2 years
    This answer has a bug, when the Adapter performs the notifyItemRemoved animation, the removed item's will flicker. You need to use the following code to get the correct position in getItemOffsets. int position = parent.getChildAdapterPosition(view); // item position if (position == RecyclerView.NO_POSITION) { position = parent.getChildLayoutPosition(view); } if (position == RecyclerView.NO_POSITION) { return; }