Android Recyclerview GridLayoutManager column spacing
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
int spanCount = 3; // 3 columns
int spacing = 50; // 50px
boolean includeEdge = false;
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));
2. with edge
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"/>
Related videos on Youtube
![Nick H](https://i.stack.imgur.com/sc72z.jpg?s=256&g=1)
Comments
-
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.
-
CommonsWare over 9 yearsHave you tried subclassing
GridLayoutManager
and overridinggenerateDefaultLayoutParams()
and kin? -
Nick H over 9 yearsI have not, I thought there would have been a method I was just not seeing to set the spacing the like grid view. I will try that
-
hch about 5 years
-
Arpit J. over 4 years
-
-
Amio.io about 9 yearsUse 'outRect.top = space' and remove 'outRect.bottom' if you don't want to mess with the 'if for the first position'. ;-]
-
ianhanniballake about 9 years@zatziky - yep, if you already use top and bottom padding as part of your
RecyclerView
(and useclipToPadding="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 about 9 yearsI'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 about 9 yearsAt the time of scrolling RecyclerView, spacing changed.
-
yqritc about 9 yearsI 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 almost 9 yearsI 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 almost 9 yearsWorks unless you have items that have various spans, like headers.
-
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 almost 9 yearsIt worth to mention, that you have to modify all fields of outRect. otherwise you can get the offset of the previous view.
-
Samir almost 9 yearsThis 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 almost 9 yearsThe
SpaceItemDecoration
actually adds the padding to the parent (the recycler view). -
Samir almost 9 yearsonly
halfSpace
padding appeared(to the right side) when I had not set the padding to the parent in xml -
Mark Hetherington almost 9 yearsIt 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 almost 9 yearsWell I don't have any padding set in the xml.
-
Pirdad Sakhizada over 8 years@yqritc thanks for nocticing parent.getChildCount(). I've updated my answer to use the parent.getLayoutManager().getItemCount()
-
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 over 8 yearsIf 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 over 8 yearsGreat answer; one tip: spacing is in px (so you can convert dp to px using Math.round(someDpValue * getResources().getDisplayMetrics().density))
-
Haris Qurashi over 8 yearsIts 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 over 8 yearsWorks great! But I have some problems with StaggeredGridLayoutManager. imgur.com/XVutH5u horizontal margins sometimes differs.
-
Admin about 8 yearsHow 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 about 8 years@HarisQureshi I had same problem. You have to remove item decoration every time you change spanCount.
-
Meanman about 8 yearsWorks 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 almost 8 yearsYou should use
StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); int position = lp.getSpanIndex();
to get the position instead ofparent.getChildAdapterPosition(view);
this will solve the issues with bad calculations. -
Xingxing over 7 yearsevery one could copy code from gist.github.com/xingstarx/f2525ef32b04a5e67fecc5c0b5c4b939
-
Vulovic Vukasin over 7 yearsBest 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 over 7 yearsgetChildAdapterPosition (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 over 7 yearsI 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 over 7 yearsHi. This works amazing but I am using a header with your solution. Can you suggest how can achieve full-width header?
-
Praneeth over 7 yearswhat is mMargin you are passing in? what value should i pass
-
Mark Hetherington over 7 years@praneethkumar that number is spacing in pixels you want
-
Masoud Mohammadi almost 7 yearsThis 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 over 6 yearsThis worked very well out of the box even with variable spans, congratulations and thank you!
-
Konstantin Konopko over 6 yearsDo not forget set grid item root layout
layout_width="match_parent"
-
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 over 6 yearswill 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 over 6 yearsthe size of item will fixed but the space between item may different, you can see too 2 images above for understand
-
Sunil over 6 yearsThey look different on different screen sizes.any way but working thank you
-
Sahil Patel about 6 yearsIf 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 about 6 yearsWhat is headerNum?
-
salmanseifian almost 6 yearsthis
parent.getChildLayoutPosition(view) == 0
puts second item in first row a little lower. I think it should be edited -
Kuva over 5 yearsThis 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 over 5 yearsexcellent calculation for the left & right. perfect same spacing
-
Shirane85 over 5 yearsGreat answer man! Works for all cases, including not symmetrical "regular" GridLayoutManager where you have an header item between items. Thanks!
-
B.shruti about 5 yearsPerfect, it's simple and effective.
-
Avi about 5 yearsThis is the more generic answer and should have been the marked one
-
hch about 5 yearsThis 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 over 4 yearsIt works fine! could you elaborate on the issue, please?
-
elyar abad over 4 yearsThank 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 over 4 yearskindly 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 over 4 yearsyou didn't consider span count
-
zeleven about 4 yearsIt 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 setright
for only last one? -
massivemadness about 4 yearsI would add following lines, if someone like me using GridLayoutManager with spanCount=1
columnCount == 1 -> { outRect.left = space outRect.right = space }
-
Amir Ziarati almost 4 yearsonly 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 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 over 3 yearsperfect solution!
-
Mahdi Safarmohammadloo over 3 yearsbetween space is 2x ! its problem !
-
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 almost 3 yearsInteresting library, I'll give it a try.
-
Xam almost 3 yearsWell, 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 almost 3 years@Xam Glad you liked it.
-
Naveen Kumawat over 2 yearsBro, you're genius _/.
-
LXJ over 2 yearsfor some reason I have to set outRect.bottom and outRect.top to -20 to get to some acceptable row spacing.
-
CoolMind over 2 yearsWhat is it? A simple vertical RV without grid?
-
Remc4 over 2 yearsAdd 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 about 2 yearsThis 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 ingetItemOffsets
.int position = parent.getChildAdapterPosition(view); // item position if (position == RecyclerView.NO_POSITION) { position = parent.getChildLayoutPosition(view); } if (position == RecyclerView.NO_POSITION) { return; }