RecyclerView GridLayoutManager: how to auto-detect span count?
Solution 1
Personaly I don't like to subclass RecyclerView for this, because for me it seems that there is GridLayoutManager's responsibility to detect span count. So after some android source code digging for RecyclerView and GridLayoutManager I wrote my own class extended GridLayoutManager that do the job:
public class GridAutofitLayoutManager extends GridLayoutManager
{
private int columnWidth;
private boolean isColumnWidthChanged = true;
private int lastWidth;
private int lastHeight;
public GridAutofitLayoutManager(@NonNull final Context context, final int columnWidth) {
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1);
setColumnWidth(checkedColumnWidth(context, columnWidth));
}
public GridAutofitLayoutManager(
@NonNull final Context context,
final int columnWidth,
final int orientation,
final boolean reverseLayout) {
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1, orientation, reverseLayout);
setColumnWidth(checkedColumnWidth(context, columnWidth));
}
private int checkedColumnWidth(@NonNull final Context context, final int columnWidth) {
if (columnWidth <= 0) {
/* Set default columnWidth value (48dp here). It is better to move this constant
to static constant on top, but we need context to convert it to dp, so can't really
do so. */
columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
context.getResources().getDisplayMetrics());
}
return columnWidth;
}
public void setColumnWidth(final int newColumnWidth) {
if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
columnWidth = newColumnWidth;
isColumnWidthChanged = true;
}
}
@Override
public void onLayoutChildren(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) {
final int width = getWidth();
final int height = getHeight();
if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
final int totalSpace;
if (getOrientation() == VERTICAL) {
totalSpace = width - getPaddingRight() - getPaddingLeft();
} else {
totalSpace = height - getPaddingTop() - getPaddingBottom();
}
final int spanCount = Math.max(1, totalSpace / columnWidth);
setSpanCount(spanCount);
isColumnWidthChanged = false;
}
lastWidth = width;
lastHeight = height;
super.onLayoutChildren(recycler, state);
}
}
I don't actually remember why I choosed to set span count in onLayoutChildren, I wrote this class some time ago. But the point is we need to do so after view get measured. so we can get it's height and width.
EDIT 1: Fix error in code caused to incorrectly setting span count. Thanks user @Elyees Abouda for reporting and suggesting solution.
EDIT 2: Some small refactoring and fix edge case with manual orientation changes handling. Thanks user @tatarize for reporting and suggesting solution.
Solution 2
I accomplished this using a view tree observer to get the width of the recylcerview once rendered and then getting the fixed dimensions of my card view from resources and then setting the span count after doing my calculations. It is only really applicable if the items you are displaying are of a fixed width. This helped me automatically populate the grid regardless of screen size or orientation.
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int viewWidth = mRecyclerView.getMeasuredWidth();
float cardViewWidth = getActivity().getResources().getDimension(R.dimen.cardview_layout_width);
int newSpanCount = (int) Math.floor(viewWidth / cardViewWidth);
mLayoutManager.setSpanCount(newSpanCount);
mLayoutManager.requestLayout();
}
});
Solution 3
Well, this is what I used, fairly basic, but gets the job done for me. This code basically gets the screen width in dips and then divides by 300 (or whatever width you're using for your adapter's layout). So smaller phones with 300-500 dip width only display one column, tablets 2-3 columns etc. Simple, fuss free and without downside, as far as I can see.
Display display = getActivity().getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
float density = getResources().getDisplayMetrics().density;
float dpWidth = outMetrics.widthPixels / density;
int columns = Math.round(dpWidth/300);
mLayoutManager = new GridLayoutManager(getActivity(),columns);
mRecyclerView.setLayoutManager(mLayoutManager);
Solution 4
I extended the RecyclerView and overrode the onMeasure method.
I set an item width(member variable) as early as I can,with a default of 1. This also updates on configuration changed. This will now have as many rows as can fit in portrait,landscape,phone/tablet etc.
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
int width = MeasureSpec.getSize(widthSpec);
if(width != 0){
int spans = width / mItemWidth;
if(spans > 0){
mLayoutManager.setSpanCount(spans);
}
}
}
Solution 5
I'm posting this just in case someone gets weird column width as in my case.
I'm not able to comment on @s-marks's answer due to my low reputation. I applied his solution solution but I got some weird column width, so I modified checkedColumnWidth function as follows:
private int checkedColumnWidth(Context context, int columnWidth)
{
if (columnWidth <= 0)
{
/* Set default columnWidth value (48dp here). It is better to move this constant
to static constant on top, but we need context to convert it to dp, so can't really
do so. */
columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
context.getResources().getDisplayMetrics());
}
else
{
columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, columnWidth,
context.getResources().getDisplayMetrics());
}
return columnWidth;
}
By converting the given column width into DP fixed the issue.
foo64
Updated on July 17, 2021Comments
-
foo64 almost 3 years
Using the new GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html
It takes an explicit span count, so the problem now becomes: how do you know how many "spans" fit per row? This is a grid, after all. There should be as many spans as the RecyclerView can fit, based on measured width.
Using the old
GridView
, you would just set the "columnWidth" property and it would automatically detect how many columns fit. This is basically what I want to replicate for the RecyclerView:- add OnLayoutChangeListener on the
RecyclerView
- in this callback, inflate a single 'grid item' and measure it
- spanCount = recyclerViewWidth / singleItemWidth;
This seems like pretty common behavior, so is there a simpler way that I'm not seeing?
- add OnLayoutChangeListener on the
-
fikr4n over 9 yearsI used this and got
ArrayIndexOutOfBoundsException
(at android.support.v7.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:361)) when scrolling theRecyclerView
. -
alvinmeimoun over 9 yearsJust add mLayoutManager.requestLayout() after setSpanCount() and it work
-
pez over 9 yearsNote:
removeGlobalOnLayoutListener()
is deprecated in API level 16. useremoveOnGlobalLayoutListener()
instead. Documentation. -
CommonsWare over 9 years+1 Chiu-ki Chan has a blog post outlining this approach and a sample project for it as well.
-
string.Empty over 9 yearsThe space on the right is not a bug. if the span count is 5 and
getSpanSize
returns 3 there will be a space because you are not filling the span. -
foo64 about 9 yearsWhy use screen width instead of the RecyclerView's width? And hardcoding 300 is bad practice (it needs to be kept in sync with your xml layout)
-
mewa almost 9 yearsThis should be the accepted answer, it's
LayoutManager
's job to lay the children out and notRecyclerView
's -
Shubham over 8 yearsWhat if
ColumnWidth
is not fixed. -
s.maks over 8 years@Shubham do you mean situation when column width should differ depending on orientation for example?
-
Shubham over 8 years@s.maks: I mean columnWidth would differ depending on the data passed into the adapter. Say if four letters words are passed then it should accommodate four items in a row if 10 letters words are passed then it should be able to accommodate only 2 items in a row.
-
s.maks over 8 years@Shubham, oh. I see. Well, I think you should not add this functionality to LayoutManager, because basically LayoutManager should not deal with actual data. It only knows "I must layout something depending on my parameters" and it knows nothing about this "something". So I think in this case you should write this logic outside of LayoutManager, in Activity.onCreate() eg. How to do so? It might be complicated depending of data passed to your adapter and actual requirenments. I suggest you to create separate question here for that, so anybody could easily find an answer later.
-
Philip Giuliani over 8 years@foo64 in the xml you can just set match_parent on the item. But yes, its still ugly ;)
-
Oleksandr Albul over 8 years@s.maks I've got an exception inside setSpanCount: java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
-
s.maks over 8 years@OleksandrAlbul do you manually calling setSpanCount(int) on layout manager? Also from where do you call setLayoutManager(LayoutManager) on your RecyclerView?
-
azizbekian about 8 yearsThe issues in this code is the following: if columnWidth = 300, and totalSpace = 736, then spanCount =2, which results in laying out items not in proportion. 2 * 300 = 600, the rest 136 pixels are not counted, which results not equal padding
-
s.maks about 8 years@azizbekian, I'm not sure problem occures because of this code, but I can't tell for sure until I see how you setup your RecyclerView and it's items. Please, create question on SO and post some code so I can help you somehow.
-
Alexandre about 8 yearsFor me, when rotating the device, it's changing a little bit the size of the grid, shrinking 20dp. Got any info about this? Thanks.
-
s.maks about 8 years@Alexandre same thing, this code works perfectly well im my projects, so I can't tell what exactly causing problems without watching your code.
-
Alexandre about 8 years@s.maks I updated the support library to 23.3.0 and this stopped happening. Guessing it was a bug from 23.0.0. Thanks for the manager, nice snippet.
-
s.maks about 8 years@Alexandre Thank you, glad to hear your problem solved.
-
Elyess Abouda about 8 yearsSometimes
getWidth()
orgetHeight()
is 0 before the view is created, which will get a wrong spanCount (1 since totalSpace will be <=0). What I added is ignoring setSpanCount in this case. (onLayoutChildren
will be called again later) -
Jimit Patel almost 8 yearsThis code is fixing width for each element and once it gets span count it gets fixed forever. I was looking for something like
WRAP_CONTENT
for each element and different span count for different row. -
Rushi M Thakker almost 7 yearsI am getting OOM error at super.onLayoutChildren(). Any help is appreciated
-
Theo over 6 yearstypo removeOnGLobalLayoutListener should be removeOnGlobalLayoutListener
-
androidXP over 6 yearsWhy downvote to accepted answer. should explain why downvote
-
Tatarize over 5 yearsThere's an edge condition that isn't covered. If you set the configChanges such that you handle the rotation rather than let it rebuild the entire activity, you have the odd case that the width of the recyclerview changes, while nothing else does. With changed width and height the spancount is dirty, but mColumnWidth hasn't changed, so onLayoutChildren() aborts and doesn't recalculate the now dirty values. Save the previous height and width, and trigger if it changes in a non-zero way.
-
Tatarize over 5 yearsyeah, that did throw me for a loop. really it would be just accept the resource itself. I mean that's like always what we'll be doing.
-
Charly Lafon almost 3 yearsTo me this is the best solution. The accepted answer is nice but as @azizbekian said it, the items are not equally distributed horizontally leading to a greater right padding than left padding. And I couldn't find a way to distribute columns equally in the horizontal axis with the accepted answer.