如何正确地将水平 RecyclerView 中的第一个和最后一个项目居中

How can I properly center the first and last items in a horizontal RecyclerView

Whosebug 包含很多这样的问题,但到目前为止绝对没有 100% 有效的解决方案。

我尝试了这些解决方案:

Android Centering Item in RecyclerView

How to make recycler view start adding items from center?

How to snap to particular position of LinearSnapHelper in horizontal RecyclerView?


并且每次第一项都无法正确居中。

我正在使用的示例代码

    recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
        @Override
        public void getItemOffsets(
            @NonNull Rect outRect,
            @NonNull View view,
            @NonNull RecyclerView parent,
            @NonNull RecyclerView.State state
        ) {

            super.getItemOffsets(outRect, view, parent, state);

            final int count = state.getItemCount();
            final int position = parent.getChildAdapterPosition(view);

            if (position == 0 || position == count - 1) {

                int offset = (int) (parent.getWidth() * 0.5f - view.getWidth() * 0.5f);

                if (position == 0) {
                    setupOutRect(outRect, offset, true);
                } else if (position == count - 1) {
                    setupOutRect(outRect, offset, false);
                }

            }

        }

        private void setupOutRect(Rect rect, int offset, boolean start) {
            if (start) {
                rect.left = offset;
            } else {
                rect.right = offset;
            }
        }

    });

查了之后发现是因为getItemOffsets的时候view.getWidth是0,还没有测到。

我试图强制测量,但每次都给出错误的尺寸,与实际尺寸完全不同,它更小。

我也尝试使用 addOnGlobalLayoutListener 技巧,但是当它被调用并且宽度正确时,outRect 已经被消耗,所以它丢失了。

我不想设置任何固定大小,因为 RecyclerView 中的项目可以有不同的大小,因此不能提前设置其填充。

我也不想添加“幽灵”项来填充space,而且这些项也不能很好地用于滚动体验。

我怎样才能让它正常工作?

理想情况下,ItemDecorator 方法看起来是最好的,但对于第一项,它立即表现平平。

据我了解,您希望第一个和最后一个项目位于 recyclerview 的中心。如果是这样,我会推荐一个更简单的解决方法。

public class OverlaysAdapter extends RecyclerView.Adapter<OverlaysAdapter2.CategoryViewHolder> {


private int fullWidth;//gets the recyclerview full width in constructor. In my case it is full display width.

@Override
public CategoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    FrameLayout fr = (FrameLayout) inflater.inflate(R.layout.item_sticker, parent, false);
    if (viewType==TYPE_LEFT_ITEM) {
        int marginLeft = (fullWidth-leftItemWidth)/2;
        ((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(marginLeft, 0,0,0);
    } else if (viewType==TYPE_RIGHT_ITEM) {
        int marginRight = (fullWidth-rightItemWidth)/2;
        ((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(0, 0,marginRight,0);
    } else if (viewType==TYPE_MIDDLE_ITEM) {
        ((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(0, 0,0,0);
    }
    return new CategoryViewHolder(fr);
}


private final int TYPE_LEFT_ITEM = 1;
private final int TYPE_MIDDLE_ITEM = 2;
private final int TYPE_RIGHT_ITEM = 3;

@Override
public int getItemViewType(int position) {
    if (position==0)
        return TYPE_RIGHT_ITEM;
    else if (position==items.size()-1)
        return TYPE_LEFT_ITEM;
    else
        return TYPE_MIDDLE_ITEM;
}

}

这个想法很简单。为第一项、中间项和最后项定义三种视图类型。注意项目视图类型并计算所需的边距并设置边距。

请注意,在我的示例中,左边距用于最后一项,右边距用于第一项,因为布局始终为 RTL。您可能需要使用相反的顺序。

您也可以更改 RecyclerView 本身的填充以获得此效果(只要禁用 clipToPadding)。我们可以在 LayoutManager 中拦截第一个布局阶段,这样即使在第一次布局项目时它也可以使用更新的填充:

添加此布局管理器:

open class CenterLinearLayoutManager : LinearLayoutManager {
    constructor(context: Context) : super(context)
    constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)

    private lateinit var recyclerView: RecyclerView

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        // always measure first item, its size determines starting offset
        // this must be done before super.onLayoutChildren
        if (childCount == 0 && state.itemCount > 0) {
            val firstChild = recycler.getViewForPosition(0)
            measureChildWithMargins(firstChild, 0, 0)
            recycler.recycleView(firstChild)
        }
        super.onLayoutChildren(recycler, state)
    }

    override fun measureChildWithMargins(child: View, widthUsed: Int, heightUsed: Int) {
        val lp = (child.layoutParams as RecyclerView.LayoutParams).absoluteAdapterPosition
        super.measureChildWithMargins(child, widthUsed, heightUsed)
        if (lp != 0 && lp != itemCount - 1) return
        // after determining first and/or last items size use it to alter host padding
        when (orientation) {
            HORIZONTAL -> {
                val hPadding = ((width - child.measuredWidth) / 2).coerceAtLeast(0)
                if (!reverseLayout) {
                    if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding)
                    if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
                } else {
                    if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding)
                    if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
                }
            }
            VERTICAL -> {
                val vPadding = ((height - child.measuredHeight) / 2).coerceAtLeast(0)
                if (!reverseLayout) {
                    if (lp == 0) recyclerView.updatePaddingRelative(top = vPadding)
                    if (lp == itemCount - 1) recyclerView.updatePaddingRelative(bottom = vPadding)
                } else {
                    if (lp == 0) recyclerView.updatePaddingRelative(bottom = vPadding)
                    if (lp == itemCount - 1) recyclerView.updatePaddingRelative(top = vPadding)
                }
            }
        }
    }

    // capture host recyclerview
    override fun onAttachedToWindow(view: RecyclerView) {
        recyclerView = view
        super.onAttachedToWindow(view)
    }
}

然后将其用于您的 RecyclerView:

recyclerView.layoutManager = CenterLinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerView.clipToPadding = false // disabling clip to padding is critical

我一直在使用 CenterLinearLayoutManager,到目前为止它几乎完美运行。我说几乎,不是因为它不能立即正常工作,而是因为我最终在使用上述布局管理器的 RecyclerView 中使用了 LinearSnapHelper

因为 snap 助手依赖于知道 RecyclerView 填充来计算它的正确中心,所以只为第一个项目设置填充会抛出这个过程,导致第一个项目(和后续项目直到最后一个实际上显示)从中心偏移。

我的解决方案是确保在显示第一个项目时从一开始就正确设置两个填充。

所以这个:

if (!reverseLayout) {
    if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding)
    if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
  } else {
    if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding)
    if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
  }

变成这样

if (!reverseLayout) {
    if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding, end = hPadding) // here we set the same padding for both sides
    if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
   } else {
    if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding, start = hPadding) // here we set the same padding for both sides
    if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
   }

而且我假设同样的逻辑也必须应用于垂直块。

我相信这现在可以进一步优化,所以上面的最后一个块看起来像这样:

if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding, end = hPadding) // here we set the same padding for both sides
if (lp == itemCount - 1) {
   if (!reverseLayout) recyclerView.updatePaddingRelative(end = hPadding)
   if (reverseLayout) recyclerView.updatePaddingRelative(start = hPadding)
}

注意:我最终发现,如果我使用的项目之间存在明显的宽度差异,那么我在此处提到的相同问题也会在最后一个项目加载时发生,这是有道理的,因为此时我们正在对一个项目设置填充一侧与另一侧不同,再次抛出原生中心计算。

这个解决方案是在加载第一个项目和最后一个项目时为两侧设置相同的填充,就像这样

recyclerView.updatePaddingRelative(start = hPadding, end = hPadding)

字面上就是这样,没有像上一个示例那样的 ifs。

当然,这仍然不能解决当显示的项目太少以至于第一个和最后一个项目可见时的问题,但是当我设法找到针对该特定情况的解决方案时,我会在这里更新。