Android 使用 gridlayoutmanager 添加元素的最后一个元素的间距

我试图用 GridLayoutManagerRecyclerView的最后一个元素行下面添加间距。为此,我使用了定制的 ItemDecoration,其最后一个元素的底部填充如下:

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;
}
}
}

但是这个方法的问题在于它搞乱了最后一行网格中的元素高度。我猜测 GridLayoutManager会根据左边的间距改变元素的高度。实现这一目标的正确方法是什么?

这将工作正确的一个 LinearLayoutManager。只是在情况下,一个 GridLayoutManager它的问题。

它非常有用的情况下,你有一个 FAB在底部,需要在最后一行的项目,以上滚动 FAB,使他们可以看到。

49220 次浏览

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;

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);
}

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.

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!

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

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 */;
}
}
}

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" />

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

android:clipToPadding="false"

🍑 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);
<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