如果不需要滚动,是否可以将 RecyclerView 中的最后一项停靠到底部?

Is it possible to have the last item in a RecyclerView to be docked to the bottom if there is no need to scroll?

我正在构建一个购物车 RecyclerView,它在 RecyclerView 中显示购物车中的所有商品,并且在底部有一个额外的视图,用于汇总购物车(欠款总额、优惠券折扣(如果适用,等等) ).

如果购物车中的商品超过 3 件,则看起来没问题,因为用户必须滚动到底部才能查看 "summary view"。但是,如果有 1 或 2 个项目,第一个项目出现,而不是摘要视图,然后是空白。我宁愿第一个项目,然后是空格,然后是摘要视图。

我试过添加空项目,但是,根据设备的分辨率,它看起来不一致。

当前外观如果少于 3 个项目(即如果不需要滚动):

-----------
|  Item1  |
| ------- |
|  Item2  |
| ------- |
| Summary |
| ------- |
|         |
|         |
|         |
|         |
 ----------

期望的外观:

-----------
|  Item1  |
| ------- |
|  Item2  |
| ------- |
|         |
|         |
|         |
|         |
| ------- |
| Summary |
 ----------

这不是预期的用例,但您创建了一个 ItemDecorator,当在底部视图上调用 getItemOffsets 时,计算其上方的 space 并将其设置为 decorOffsetTop。

每次添加项目时,您应该使项目装饰偏移量无效,以便 RecyclerView 将再次调用您的回调进行新的计算。

这不会是微不足道的,但这应该行得通。

我正在考虑您的任务,并最终整理了一些您可能会觉得有用的代码。不过这个阶段有问题。

我所做的是向回收站视图添加项目装饰器:

recyclerView.addItemDecoration(new StickySummaryDecoration());

这是我对基本装饰器的实现(坦率地说,这是我第一次使用项目装饰器,所以它可能不是最佳的或完全正确的,但我已经尽力了):

public class StickySummaryDecoration extends RecyclerView.ItemDecoration {

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int childCount = parent.getAdapter().getItemCount();
        int lastVisibleItemPosition =
                ((LinearLayoutManager) parent.getLayoutManager()).findLastVisibleItemPosition();
        int firstVisiblePosition =
                ((LinearLayoutManager) parent.getLayoutManager())
                        .findFirstCompletelyVisibleItemPosition();
        if ((firstVisiblePosition == 0) && (lastVisibleItemPosition == (childCount - 1))) {
            View summaryView = parent.getChildAt(parent.getChildCount() - 1);
            int topOffset = parent.getHeight() - summaryView.getHeight();
            int leftOffset =
                    ((RecyclerView.LayoutParams) summaryView.getLayoutParams()).leftMargin;
            c.save();
            c.translate(leftOffset, topOffset);
            summaryView.draw(c);
            c.restore();
            summaryView.setVisibility(View.GONE);
        }
    }
}

所以我得到了什么。

短名单中的底部停靠摘要:

您需要向下滚动才能看到长列表中的摘要:

现在说一个问题。在长列表中向上滚动时的这种副作用是我尚未解决的问题。

我的实验已上传到 github repo 以防万一:)

编辑

刚才我想到回收站视图中缺少的行元素是我的回收摘要视图持有者,它具有 GONE 可见性。应该有办法把它带回来...

编辑 2

我重新考虑了我的第一个解决方案并稍微更改了代码以解决长列表的情况(在这种情况下项目装饰被禁用(我认为它更接近你想要实现的)),这会自动解决行缺失问题。

基于我创建了一个有效的实现。扩展 ItemDecoration 并覆盖 getItemOffsets():

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    int childCount = parent.getAdapter().getItemCount();
    if (parent.getChildLayoutPosition(view) != childCount - 1) return;
    int lastViewBottom = calculateViewBottom(parent.getLayoutManager().findViewByPosition(childCount - 2));

    view.measure(parent.getWidth(), parent.getHeight());
    int height = view.getMeasuredHeight();
    int topOffset = parent.getHeight() - lastViewBottom - height;
    if (topOffset < 0) topOffset = itemOffset;

    outRect.set(itemOffset, topOffset, itemOffset, itemOffset);
}

private int calculateViewBottom(View view) {
    ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
    return (int) view.getY() + view.getHeight() + params.topMargin + params.bottomMargin;
}

基于 Lamorak 的回答,我用 Kotlin 重写了代码并对其进行了修改以适应 RecyclerView 填充和项目边距:

class LastSticksToBottomItemDecoration: RecyclerView.ItemDecoration() {

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        parent.adapter?.itemCount?.let { childCount ->
            if (parent.getChildLayoutPosition(view) != childCount - 1) return
            val lastViewBottom = when (childCount) {
                1 -> 0
                else -> parent.layoutManager?.findViewByPosition(childCount - 2)?.let {
                    calculateViewBottom(it, parent)
                } ?: 0
            }
            view.measure(parent.width, parent.height)
            max(
                0,
                parent.height -
                        parent.paddingTop -
                        parent.paddingBottom -
                        lastViewBottom -
                        view.measuredHeight -
                        view.marginTop -
                        view.marginBottom
            ).let { topOffset ->
                outRect.set(0, topOffset, 0, 0)
            }
        }
    }

    private fun calculateViewBottom(view: View, parent: RecyclerView): Int =
        (view.layoutParams as ViewGroup.MarginLayoutParams).let {
            view.measure(parent.width, parent.height)
            view.y.toInt() + view.measuredHeight + it.topMargin + it.bottomMargin
        }
}