I am trying to add spacing below the last element row in RecyclerView
with GridLayoutManager
. I used custom ItemDecoration
for this purpose with bottom padding when its last element as follows:
public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
private int space;
private int bottomSpace = 0;
public SpaceItemDecoration(int space, int bottomSpace) {
this.space = space;
this.bottomSpace = bottomSpace;
}
public SpaceItemDecoration(int space) {
this.space = space;
this.bottomSpace = 0;
}
@Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
int childCount = parent.getChildCount();
final int itemPosition = parent.getChildAdapterPosition(view);
final int itemCount = state.getItemCount();
outRect.left = space;
outRect.right = space;
outRect.bottom = space;
outRect.top = space;
if (itemCount > 0 && itemPosition == itemCount - 1) {
outRect.bottom = bottomSpace;
}
}
}
But the problem with this method is that it messed up the element heights in the grid in last row. I am guessing that GridLayoutManager
changes the heights for elements based on spacing left. What is the correct way to achieve this?
This will work correctly for a LinearLayoutManager
. Just in case of a GridLayoutManager
its problematic.
Its very useful in case you have a FAB
in bottom and need items in last row to scroll above FAB
so that they can be visible.
The solution to this problem lies in overrinding the SpanSizeLookup of GridLayoutManager.
You have to make changes to the GridlayoutManager in the Activity or Fragment where you are inflating the RecylerView.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//your code
recyclerView.addItemDecoration(new PhotoGridMarginDecoration(context));
// SPAN_COUNT is the number of columns in the Grid View
GridLayoutManager gridLayoutManager = new GridLayoutManager(context, SPAN_COUNT);
// With the help of this method you can set span for every type of view
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (list.get(position).getType() == TYPE_HEADER) {
// Will consume the whole width
return gridLayoutManager.getSpanCount();
} else if (list.get(position).getType() == TYPE_CONTENT) {
// will consume only one part of the SPAN_COUNT
return 1;
} else if(list.get(position).getType() == TYPE_FOOTER) {
// Will consume the whole width
// Will take care of spaces to be left,
// if the number of views in a row is not equal to 4
return gridLayoutManager.getSpanCount();
}
return gridLayoutManager.getSpanCount();
}
});
recyclerView.setLayoutManager(gridLayoutManager);
}
Just add a padding and set android:clipToPadding="false"
<RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:clipToPadding="false" />
Thanks to this wonderful answer!
What you can do is add an empty footer to your recyclerview. Your padding will be the size of your footer.
@Override
public Holder onCreateViewHolder( ViewGroup parent, int viewType) {
if (viewType == FOOTER) {
return new FooterHolder();
}
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
return new Holder(view);
}
@Override
public void onBindViewHolder(final Holder holder, final int position) {
//if footer
if (position == items.getSize() - 1) {
//do nothing
return;
}
//do regular object bindding
}
@Override
public int getItemViewType(int position) {
return (position == items.getSize() - 1) ? FOOTER : ITEM_VIEW_TYPE_ITEM;
}
@Override
public int getItemCount() {
//add one for the footer
return items.size() + 1;
}
You can use the code below to detect first and last rows in a grid view and set top and bottom offsets correspondingly.
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
LayoutParams params = (LayoutParams) view.getLayoutParams();
int pos = params.getViewLayoutPosition();
int spanCount = mGridLayoutManager.getSpanCount();
boolean isFirstRow = pos < spanCount;
boolean isLastRow = state.getItemCount() - 1 - pos < spanCount;
if (isFirstRow) {
outRect.top = top offset value here
}
if (isLastRow) {
outRect.bottom = bottom offset value here
}
}
// you also need to keep reference to GridLayoutManager to know the span count
private final GridLayoutManager mGridLayoutManager;
With things like this, it's recommended to solve using the ItemDecoration as they are meant for that.
public class ListSpacingDecoration extends RecyclerView.ItemDecoration {
private static final int VERTICAL = OrientationHelper.VERTICAL;
private int orientation = -1;
private int spanCount = -1;
private int spacing;
public ListSpacingDecoration(Context context, @DimenRes int spacingDimen) {
spacing = context.getResources().getDimensionPixelSize(spacingDimen);
}
public ListSpacingDecoration(int spacingPx) {
spacing = spacingPx;
}
@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) {
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 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 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);
}
}
I copied an edited from my original answer here which is actually for equal spacing but it's the same concept.