Android add spacing below last element in recyclerview with gridlayoutmanager
Asked Answered
S

13

114

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.

Slotter answered 16/6, 2015 at 11:19 Comment(0)
A
12

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);
}
Attempt answered 18/9, 2015 at 21:24 Comment(0)
A
539

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!

Avast answered 19/3, 2016 at 11:17 Comment(8)
This did it for me. Quick 'n easy solution when you need to equally space items in the recycler. Thanks.Inoffensive
I don't believe I try to hack the layout param to add margin on the last ViewHolder :'(Calumnious
This is wonderful, but it does mess up fading edges, e.g. android:requiresFadingEdge="vertical" or recyclerView.setVerticalFadingEdgeEnabled( true );Emrich
This doesn't extend the scrollbar of the recycler view to the very bottom. Edit: to prevent this add android:scrollbarStyle="outsideOverlay"Schnapps
that's absolutely what I was looking for, thanks! this also should be the accepted answer...Homoiousian
when using android:requiresFadingEdge="vertical", need to implements RecyclerView.ItemDecoratoion subclass for spacing. getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) override method can be add space. just add outRect.bottom=spaceBoulder
This should be the answer, plus, it works for all kinds of LayoutMAnagers!Endomorph
While this works in most cases, once you get to have more complex things to handle, it can cause issues. I've written more about this on my answer, here: https://mcmap.net/q/188384/-android-add-spacing-below-last-element-in-recyclerview-with-gridlayoutmanagerPerrins
A
12

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);
}
Attempt answered 18/9, 2015 at 21:24 Comment(0)
O
12

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" />  
Oscillogram answered 18/1, 2020 at 21:26 Comment(1)
underrated solution :( It is working great for me.Appendicectomy
U
9

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 */;
        }
    }
}
Unrefined answered 3/10, 2019 at 8:6 Comment(0)
S
4
<androidx.recyclerview.widget.RecyclerView
    ...
    android:clipToPadding="false"
    android:paddingTop="16dp"
    android:paddingBottom="16dp"
    ...
    />

android:clipToPadding="false"

Serenity answered 17/5, 2022 at 18:45 Comment(0)
C
3

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;
Coerce answered 14/8, 2015 at 18:23 Comment(0)
P
3

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
    }
}
Perrins answered 23/1, 2021 at 11:10 Comment(0)
T
2

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;
}
Theodicy answered 16/6, 2015 at 16:34 Comment(2)
This is a good option. However my problem with it is I am using a custom ItemDecoration already which works based on item positions in recyclerview and decides where to put what spacing and divider etc. Now if I add this spacing as additional element, my ItemDecoration counts it as an element and adds divider to the spacing as well. So I was thinking if anybody has tried using a custom ItemDecoration to solve the problem.Slotter
Also your method will work well in the case of LinearLayoutManager or in case of GridLayoutManager if the bottom row of grid is full. Otherwise the space will go into the grid in empty element space and not at the bottom of whole grid.Slotter
F
1

🍑 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);
Fixity answered 2/9, 2022 at 13:38 Comment(0)
I
0

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.

Inseminate answered 24/12, 2015 at 4:18 Comment(0)
R
0

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()

Rockabilly answered 30/9, 2019 at 20:17 Comment(0)
B
0
<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
Bremerhaven answered 2/11, 2022 at 12:41 Comment(0)
D
0

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

android:clipToPadding="false"
android:paddingBottom="80dp"
Darlenedarline answered 16/8, 2023 at 6:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.