Looking at the possible solutions, I'd like to write some notes and a proper solution:
If you use padding for the RecyclerView, it should work in most cases, but once you start using customized item-decorations (of fast-scroller) or handle transparent navigation bar, you will get to see various issues.
You can always create a ViewType for the RecyclerView adapter that will be the footer of it, taking the entire span. This works perfectly but just requires a bit more work than other solutions.
The solution of the ItemDecoration that was offered here works in most cases, but not well for GridLayoutManager, as it sometimes add spacing for the row above the last one (requested here to add a nicer solution).
I've found some code on android-x that seems to solve it, though, somehow related to car:
BottomOffsetDecoration
/**
* A {@link RecyclerView.ItemDecoration} that will add a bottom offset to the last item in the
* RecyclerView it is added to.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class BottomOffsetDecoration extends RecyclerView.ItemDecoration {
private int mBottomOffset;
public BottomOffsetDecoration(int bottomOffset) {
mBottomOffset = bottomOffset;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
RecyclerView.Adapter adapter = parent.getAdapter();
if (adapter == null || adapter.getItemCount() == 0) {
return;
}
if (parent.getLayoutManager() instanceof GridLayoutManager) {
if (GridLayoutManagerUtils.isOnLastRow(view, parent)) {
outRect.bottom = mBottomOffset;
}
} else if (parent.getChildAdapterPosition(view) == adapter.getItemCount() - 1) {
// Only set the offset for the last item.
outRect.bottom = mBottomOffset;
} else {
outRect.bottom = 0;
}
}
/** Sets the value to use for the bottom offset. */
public void setBottomOffset(int bottomOffset) {
mBottomOffset = bottomOffset;
}
/** Returns the set bottom offset. If none has been set, then 0 will be returned. */
public int getBottomOffset() {
return mBottomOffset;
}
}
GridLayoutManagerUtils
/**
* Utility class that helps navigating in GridLayoutManager.
*
* <p>Assumes parameter {@code RecyclerView} uses {@link GridLayoutManager}.
*
* <p>Assumes the orientation of {@code GridLayoutManager} is vertical.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class GridLayoutManagerUtils {
private GridLayoutManagerUtils() {}
/**
* Returns the number of items in the first row of a RecyclerView that has a
* {@link GridLayoutManager} as its {@code LayoutManager}.
*
* @param recyclerView RecyclerView that uses GridLayoutManager as LayoutManager.
* @return number of items in the first row in {@code RecyclerView}.
*/
public static int getFirstRowItemCount(RecyclerView recyclerView) {
GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager();
int itemCount = recyclerView.getAdapter().getItemCount();
int spanCount = manager.getSpanCount();
int spanSum = 0;
int numOfItems = 0;
while (numOfItems < itemCount && spanSum < spanCount) {
spanSum += manager.getSpanSizeLookup().getSpanSize(numOfItems);
numOfItems++;
}
return numOfItems;
}
/**
* Returns the span index of an item.
*/
public static int getSpanIndex(View item) {
GridLayoutManager.LayoutParams layoutParams =
((GridLayoutManager.LayoutParams) item.getLayoutParams());
return layoutParams.getSpanIndex();
}
/**
* Returns whether or not the given view is on the last row of a {@code RecyclerView} with a
* {@link GridLayoutManager}.
*
* @param view The view to inspect.
* @param parent {@link RecyclerView} that contains the given view.
* @return {@code true} if the given view is on the last row of the {@code RecyclerView}.
*/
public static boolean isOnLastRow(View view, RecyclerView parent) {
return getLastItemPositionOnSameRow(view, parent) == parent.getAdapter().getItemCount() - 1;
}
/**
* Returns the position of the last item that is on the same row as input {@code view}.
*
* @param view The view to inspect.
* @param parent {@link RecyclerView} that contains the given view.
*/
public static int getLastItemPositionOnSameRow(View view, RecyclerView parent) {
GridLayoutManager layoutManager = ((GridLayoutManager) parent.getLayoutManager());
GridLayoutManager.SpanSizeLookup spanSizeLookup = layoutManager.getSpanSizeLookup();
int spanCount = layoutManager.getSpanCount();
int lastItemPosition = parent.getAdapter().getItemCount() - 1;
int currentChildPosition = parent.getChildAdapterPosition(view);
int spanSum = getSpanIndex(view) + spanSizeLookup.getSpanSize(currentChildPosition);
// Iterate to the end of the row starting from the current child position.
while (currentChildPosition <= lastItemPosition && spanSum <= spanCount) {
spanSum += spanSizeLookup.getSpanSize(currentChildPosition + 1);
if (spanSum > spanCount) {
return currentChildPosition;
}
currentChildPosition++;
}
return lastItemPosition;
}
}
So, I've made a Kotlin solution to solve it all (sample&library available here) :
//https://androidx.de/androidx/car/widget/itemdecorators/BottomOffsetDecoration.html
class BottomOffsetDecoration(private val mBottomOffset: Int, private val layoutManagerType: LayoutManagerType) : ItemDecoration() {
enum class LayoutManagerType {
GRID_LAYOUT_MANAGER, LINEAR_LAYOUT_MANAGER
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
when (layoutManagerType) {
LayoutManagerType.LINEAR_LAYOUT_MANAGER -> {
val position = parent.getChildAdapterPosition(view)
outRect.bottom =
if (state.itemCount <= 0 || position != state.itemCount - 1)
0
else
mBottomOffset
}
LayoutManagerType.GRID_LAYOUT_MANAGER -> {
val adapter = parent.adapter
outRect.bottom =
if (adapter == null || adapter.itemCount == 0 || GridLayoutManagerUtils.isOnLastRow(view, parent))
0
else
mBottomOffset
}
}
}
}
//https://androidx.de/androidx/car/util/GridLayoutManagerUtils.html
/**
* Utility class that helps navigating in GridLayoutManager.
*
*
* Assumes parameter `RecyclerView` uses [GridLayoutManager].
*
*
* Assumes the orientation of `GridLayoutManager` is vertical.
*/
object GridLayoutManagerUtils {
/**
* Returns whether or not the given view is on the last row of a `RecyclerView` with a
* [GridLayoutManager].
*
* @param view The view to inspect.
* @param parent [RecyclerView] that contains the given view.
* @return `true` if the given view is on the last row of the `RecyclerView`.
*/
fun isOnLastRow(view: View, parent: RecyclerView): Boolean {
return getLastItemPositionOnSameRow(view, parent) == parent.adapter!!.itemCount - 1
}
/**
* Returns the position of the last item that is on the same row as input `view`.
*
* @param view The view to inspect.
* @param parent [RecyclerView] that contains the given view.
*/
private fun getLastItemPositionOnSameRow(view: View, parent: RecyclerView): Int {
val layoutManager = parent.layoutManager as GridLayoutManager
val spanSizeLookup = layoutManager.spanSizeLookup
val spanCount = layoutManager.spanCount
val lastItemPosition = parent.adapter!!.itemCount - 1
var currentChildPosition = parent.getChildAdapterPosition(view)
val itemSpanIndex = (view.layoutParams as GridLayoutManager.LayoutParams).spanIndex
var spanSum = itemSpanIndex + spanSizeLookup.getSpanSize(currentChildPosition)
// Iterate to the end of the row starting from the current child position.
while (currentChildPosition <= lastItemPosition && spanSum <= spanCount) {
spanSum += spanSizeLookup.getSpanSize(currentChildPosition + 1)
if (spanSum > spanCount)
return currentChildPosition
++currentChildPosition
}
return lastItemPosition
}
}