【问题标题】:Android add spacing below last element in recyclerview with gridlayoutmanagerAndroid使用gridlayoutmanager在recyclerview中的最后一个元素下方添加间距
【发布时间】:2015-09-01 04:25:48
【问题描述】:

我正在尝试使用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 上方以便它们可见,那么它非常有用。

【问题讨论】:

    标签: android android-recyclerview gridlayoutmanager


    【解决方案1】:

    只需添加一个填充并设置android:clipToPadding="false"

    <RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="8dp"
        android:clipToPadding="false" />
    

    感谢这个精彩的answer

    【讨论】:

    • 这是为我做的。当您需要在回收站中均匀放置物品时,快速简便的解决方案。谢谢。
    • 这正是我想要的!谢谢!
    • 伟大而简单的解决方案。谢谢!
    • 这很棒,但它确实会弄乱褪色的边缘,例如android:requiresFadingEdge="vertical" 或 recyclerView.setVerticalFadingEdgeEnabled( true );
    • 这不会将回收站视图的滚动条扩展到最底部。编辑:为防止这种情况添加android:scrollbarStyle="outsideOverlay"
    【解决方案2】:

    这个问题的解决方法在于覆盖GridLayoutManager的SpanSizeLookup。

    您必须在要为 RecylerView 充气的 Activity 或 Fragment 中更改 GridlayoutManager。

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

    【讨论】:

      【解决方案3】:

      如果仅最后一项,则应在 Recycler View 中使用装饰作为底部边距

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

      【讨论】:

        【解决方案4】:

        我有类似的问题并回复another thread in stack overflow。为了帮助登陆此页面的其他人,我将在此处重新发布。

        在阅读了所有其他回复后,我发现 recyclerview 布局 xml 中的更改按预期对我的回收器视图起作用:

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

        完整的布局如下:

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

        【讨论】:

          【解决方案5】:

          您可以使用下面的代码来检测网格视图中的第一行和最后一行,并相应地设置顶部和底部偏移量。

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

          【讨论】:

            【解决方案6】:

            您可以在回收站视图中添加一个空页脚。您的填充将是您的页脚的大小。

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

            【讨论】:

            • 这是一个不错的选择。但是我的问题是我正在使用自定义 ItemDecoration 已经根据 recyclerview 中的项目位置工作并决定在哪里放置什么间距和分隔符等。现在如果我将此间距添加为附加元素,我的 ItemDecoration 将其计为一个元素和也为间距添加了分隔符。所以我在想是否有人尝试使用自定义 ItemDecoration 来解决问题。
            • 如果网格的底行已满,您的方法也适用于 LinearLayoutManager 或 GridLayoutManager 的情况。否则,空间将进入空白元素空间中的网格,而不是整个网格的底部。
            【解决方案7】:

            看看可能的解决方案,我想写一些笔记和一个适当的解决方案:

            1. 如果您为 RecyclerView 使用填充,它应该在大多数情况下都可以工作,但是一旦您开始使用自定义项目装饰(快速滚动)或处理透明导航栏,您将看到各种问题。

            2. 您始终可以为 RecyclerView 适配器创建一个 ViewType,它将作为它的页脚,占据整个跨度。这非常有效,但只需要比其他解决方案更多的工作。

            3. 此处提供的 ItemDecoration 解决方案在大多数情况下都有效,但不适用于 GridLayoutManager,因为它有时会为最后一行上方的行添加间距(在此处请求添加更好的解决方案)。

            我在 android-x 上找到了一些似乎可以解决问题的代码,不过,与汽车有关:

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

            所以,我制作了一个 Kotlin 解决方案来解决所有问题(此处提供示例和库):

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

            【讨论】:

              【解决方案8】:

              对于这样的事情,建议使用 ItemDecoration 来解决,因为它们就是为此而设计的。

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

              我从我的原始答案 here 复制了一个编辑过的内容,实际上是为了等间距,但它是相同的概念。

              【讨论】:

                【解决方案9】:

                您可以从源代码中以 DividerItemDecoration.java 为例进行替换

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

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

                在drawVertical()和drawHorizo​​ntal()中

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2016-08-28
                  • 1970-01-01
                  • 2013-07-29
                  • 1970-01-01
                  • 1970-01-01
                  • 2012-06-26
                  • 2011-09-11
                  • 2022-01-08
                  相关资源
                  最近更新 更多