Android add spacing below last element in recyclerview with gridlayoutmanager

54.2k views Asked by At

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.

13

There are 13 answers

0
Dhruv Pancholi On BEST ANSWER

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);
}
2
Nicolas On

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;
}
0
android developer On

Looking at the possible solutions, I'd like to write some notes and a proper solution:

  1. 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.

  2. 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.

  3. 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
    }
}
0
Cona Mobile On
<androidx.recyclerview.widget.RecyclerView
    ...
    android:clipToPadding="false"
    android:paddingTop="16dp"
    android:paddingBottom="16dp"
    ...
    />

android:clipToPadding="false"

0
sergej shafarenka On

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;
0
Pirdad Sakhizada On

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.

0
Mena Wael On
<View
    android:layout_width="match_parent"
    android:layout_height="@dimen/_10sdp"
    android:id="@+id/space"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    android:background="@color/white"
    android:visibility="gone"
/>

add this in your custom row

In onBindviewHolder add this

if(position == getItemCount()) 
    holder.space.visibilty = View.Visible
else
    holder.space.visibilty = View.Gone
0
Hammad Zafar Bawara On

Add these attributes to recyclerview. It is working in my case

android:clipToPadding="false"
android:paddingBottom="80dp"
0
ucMedia On

The simplest solution is :

    recyclerView.setClipToPadding(false);  
    // Sets whether this ViewGroup will clip its children to
    // its padding and resize (but not clip) any EdgeEffect
    // to the padded region, if padding is present.
    // By default(true), children are clipped to the padding
    // of their parent ViewGroup.
    // * So that's why we have to set this false !


    recyclerView.setPadding(0,0,0,bottomSpaceInPX);
1
us_david On

I had similar issue and replied to another thread in stack overflow. To help others who land on this page, I will repost it here.

After reading all others replies and I found the changes in layout xml for recyclerview worked for my recycler view as expected:

android:paddingBottom="127px"
android:clipToPadding="false"
android:scrollbarStyle="outsideOverlay"  

The complete layout looks like:

<android.support.v7.widget.RecyclerView
        android:id="@+id/library_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="160px"
        android:layout_marginEnd="160px"
        tools:listitem="@layout/library_list_item" />  
0
Kamir On

You could take DividerItemDecoration.java as an example from the source code and replace

for (int i = 0; i < childCount; i++)

with

for (int i = 0; i < childCount - 1; i++)

in drawVertical() and drawHorizontal()

0
Dinesh Bagvan On

Should use Decoration In Recycler View for bottom margin in case of the last item only

recyclerView.addItemDecoration(MemberItemDecoration())

public class MemberItemDecoration extends RecyclerView.ItemDecoration {

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        // only for the last one
        if (parent.getChildAdapterPosition(view) == parent.getAdapter().getItemCount() - 1) {
            outRect.bottom = 50/* set your margin here */;
        }
    }
}
8
Vedant Agarwala On

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!