使用GridLayoutManager将RecyclerView中的项目固定在底部

3

我想要一个RecyclerView,默认情况下按从左到右、从上到下的顺序排列其项。但是我还希望它具有以下特殊行为,如下图右侧所示。

enter image description here

我希望“整体上”这些项目像RecyclerView自己会调整大小以包裹其内容一样,粘在底部 - 以便更好地使用大拇指搜索结果。我无法通过将LayoutManager::reverseLayoutRecyclerView::layoutDirectionRecyclerViewwrap_content组合起来实现此行为,使其保持在底部也没有效果,因为我希望整个列表区域可滚动(RecyclerView必须保持其完整大小)- 您有什么建议可以实现这种行为吗?


你可能可以通过添加顶部填充或虚拟条目来填充顶部行来实现这一点。未占用的顶部区域显示什么?是否有像您展示的网格模式,还是全部为空白?我假设您正在使用GridLayoutManager。 - Cheticamp
正确,我使用GridLayoutManager。网格只是象征着项目以特殊的方式粘附在底部,并且RecyclerView不会包装其内容。因此,最终网格应该是不可见的。添加虚拟项确实有效,我一直都是这样做的 - 但我可能也会这样做,因为这是我能想到的唯一解决方案。此外,如果您使用具有不同跨度计数的项目,则虚拟项目的逻辑必须处理此问题,但仍然有效。 - prom85
2个回答

1

这是我对你所尝试做的事情的理解:

  1. RecyclerView 在屏幕上有一定的宽度和高度,无论设备方向如何,都保持不变。

  2. 如果项目数量填满了 RecyclerView 的显示区域,则 RecyclerView 将填充该区域并进行滚动。

  3. 如果项目数量没有填满 RecyclerView 的显示区域,则整个项目显示应该下沉到 RecyclerView 的屏幕底部,留下顶部的空白区域。

其中一种方法是以编程方式设置 RecyclerView 的顶部填充值,该值为如果显示的项目未填满 RecyclerView 的总高度与 RecyclerView 的高度差。如果能够在布局之前计算出高度差,那会更容易些,但您也可以通过全局布局监听器在布局后计算出来。

LinearLayoutManager#findFirstCompletelyVisibleItemPosition() 可以告诉你哪个项目在显示区域的顶部。

LinearLayoutManager#findLastCompletelyVisibleItemPosition() 可以告诉你最后一个位置。

如果所有位置都显示出来了,你就知道要引入顶部填充。

如果它们不填满 RecyclerView,我不确定你是否希望这些项目能够滚动到顶部。如果应该向上滚动并留下空白空间,则引入底部填充。

你还需要在 RecyclerView 上指定 android:clipToPadding="false",才能使上述填充工作正常。

以下是一个示例活动,展示如何实现上述功能:

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private final List<String> mItems = new ArrayList<>();
    private RecyclerView mRecycler;
    private final int mCount = 24;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        for (int i = 0; i < mCount; i++) {
            mItems.add("[" + i + "] " + getString(R.string.lorem));
        }

        mRecycler = findViewById(R.id.recyclerView);
        RecyclerViewAdapter mAdapter = new RecyclerViewAdapter(mItems);
        mRecycler.setAdapter(mAdapter);
        mRecycler.setLayoutManager(new GridLayoutManager(this, 5));

        mRecycler.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mRecycler.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                GridLayoutManager glm = (GridLayoutManager) mRecycler.getLayoutManager();
                if (glm == null) return;
                int lastPosition = glm.findLastCompletelyVisibleItemPosition();
                if (lastPosition == (mCount - 1)) {
                    View lastView = glm.findViewByPosition(lastPosition);
                    if (lastView == null) return;
                    int newPadding = mRecycler.getHeight() - lastView.getBottom();
                    // Set bottom padding to newPadding to allow items to scroll to the top
                    // of the RecyclerView.
                    mRecycler.setPadding(0, newPadding, 0, newPadding);
                    mRecycler.setClipToPadding(false);
                }
            }
        });
    }
}

enter image description here


谢谢你的想法-它非常简单,但在我的测试中,它不能与RecyclerView动画和过滤一起使用(当计算填充时,视图不会处于最终位置,动画将无法正确工作)。我认为只有在自定义布局管理器中才能正确完成这项工作,因此虚拟项+反转数据+反转布局仍然是我最好且最简单的选择。 - prom85

0

要实现将RecyclerView中的所有项对齐到底部的行为,您需要设置标志setStackFromEnd(true)

当从底部堆叠设置为true时,列表从视图底部开始填充其内容

但是,这个标志不被GridLayout支持,会抛出异常,如下所示:

GridLayoutManager不支持从末尾堆栈。考虑使用反向布局

但即使在GridLayoutManager中使用setReverseLayout(true),它也会从底部向上和从左到右呈现项目,这不是您想要的结果,即使翻转列表数据。

可能的解决方案:

  1. 使用GridLayoutManager并仅向第一列添加顶部填充以填充剩余的顶部区域,甚至使用虚拟空列。

  2. 使用LinearLayout和setStackFromEnd(true),它按预期工作,其中每个项从RecyclerView的底部向上呈现,并且每个项行(Item View)保持例如:4个视图(列数)具有相等的宽度。当然,在这种方式中,列表中的每个Item模型必须保存4个数据,每个数据对应一个列。

如果您选择第二个选项,您可以像下面的代码片段一样为每行准备列数据:
List<ItemModel> data = new ArrayList<>();
int listCnt = 14;
for(int i=0; i<listCnt; i++){
    if(i%4 == 0) {
        String dataCol1 = i < listCnt ? String.valueOf(i) : "";
        String dataCol2 = (i + 1) < listCnt ? String.valueOf(i + 1) : "";
        String dataCol3 = (i + 2) < listCnt ? String.valueOf(i + 2) : "";
        String dataCol4 = (i + 3) < listCnt ? String.valueOf(i + 3) : "";
        data.add(new ItemModel(dataCol1, dataCol2, dataCol3, dataCol4));
    }
}

//set LinearLayoutManager
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, RecyclerView.VERTICAL, false);
linearLayoutManager.setStackFromEnd(true);
recyclerView.setLayoutManager(linearLayoutManager);

//set adapter
RecyclerViewAdapter adapter = new RecyclerViewAdapter(data);
recyclerView.setAdapter(adapter);

其中ItemModel保存每行数据,例如:

public class ItemModel {

    public String dataCol1="", dataCol2="", dataCol3="", dataCol4="";

    public ItemModel(String dataCol1, String dataCol2, String dataCol3, String dataCol4) {
        this.dataCol1 = dataCol1;
        this.dataCol2 = dataCol2;
        this.dataCol3 = dataCol3;
        this.dataCol4 = dataCol4;
    }
}

当然,在您的适配器中,对于那些没有任何数据的列,您必须将其可见性设置为View.INVISIBLE,而对于那些有数据的列,则需设置为View.VISIBLE。 适配器的示例如下:

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

        private List<ItemModel> mData;

        public RecyclerViewAdapter(List<ItemModel> data) {
            this.mData = data;
        }

        @NonNull
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_row, parent, false);
            return new ItemViewHolder(view);
        }

        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
           ItemModel itemModel = mData.get(position);
           if(holder instanceof ItemViewHolder) {
               ((ItemViewHolder) holder).bind(itemModel);
           }
        }

        @Override
        public int getItemCount() {
            return mData.size();
        }

        public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
            public TextView itemTextView1, itemTextView2, itemTextView3, itemTextView4;

            public ItemViewHolder(View itemView) {
                super(itemView);
                itemTextView1 = itemView.findViewById(R.id.itemTextView1);
                itemTextView2 = itemView.findViewById(R.id.itemTextView2);
                itemTextView3 = itemView.findViewById(R.id.itemTextView3);
                itemTextView4 = itemView.findViewById(R.id.itemTextView4);

                itemTextView1.setOnClickListener(this);
                itemTextView2.setOnClickListener(this);
                itemTextView3.setOnClickListener(this);
                itemTextView4.setOnClickListener(this);
            }

            public void bind(ItemModel itemModel){
                itemTextView1.setText(itemModel.dataCol1);
                itemTextView2.setText(itemModel.dataCol2);
                itemTextView3.setText(itemModel.dataCol3);
                itemTextView4.setText(itemModel.dataCol4);

                itemTextView1.setVisibility(TextUtils.isEmpty(itemModel.dataCol1) ? View.INVISIBLE : View.VISIBLE);
                itemTextView2.setVisibility(TextUtils.isEmpty(itemModel.dataCol2) ? View.INVISIBLE : View.VISIBLE);
                itemTextView3.setVisibility(TextUtils.isEmpty(itemModel.dataCol3) ? View.INVISIBLE : View.VISIBLE);
                itemTextView4.setVisibility(TextUtils.isEmpty(itemModel.dataCol4) ? View.INVISIBLE : View.VISIBLE);
            }

            @Override
            public void onClick(View v) {
                int cId = v.getId();
                if(cId == itemTextView1.getId()){
                    //get col1 data
                    String dataCol1 = mData.get(getAdapterPosition()).dataCol1;
                }
                else if(cId == itemTextView2.getId()){
                    //get col2 data
                    String dataCol2 = mData.get(getAdapterPosition()).dataCol2;
                }
                else if(cId == itemTextView3.getId()){
                    //get col3 data
                    String dataCol3 = mData.get(getAdapterPosition()).dataCol3;
                }
                else if(cId == itemTextView4.getId()){
                    //get col4 data
                    String dataCol4 = mData.get(getAdapterPosition()).dataCol4;
                }
            }
        }
    }

列表项 R.layout.item_list_row 包含 4 列视图,可以如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/itemTextView1"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_light"
        android:textColor="@android:color/black"
        tools:text="1"/>

    <TextView
        android:id="@+id/itemTextView2"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_light"
        android:textColor="@android:color/black"
        tools:text="2"/>

    <TextView
        android:id="@+id/itemTextView3"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_light"
        android:textColor="@android:color/black"
        tools:text="3"/>

    <TextView
        android:id="@+id/itemTextView4"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_light"
        android:textColor="@android:color/black"
        tools:text="4"/>

</LinearLayout>

当然,如果您有不同的跨度计数,您可以在RecyclerView适配器中创建不同的ViewHolder类型。

选项2的结果,RecyclerView高度分别为match_parent300dpwrap_content,如下所示。 RecyclerView具有灰色背景:

match_parent 300dp wrap_content


这只是一个视觉解决方案。手动组合子项不是好的解决方案 - 你会失去默认的项目处理、动画等... 我更喜欢使用 setReverseLayout + 反向数据 + 不可见的虚拟项放在最后,但这也有一些小缺点... 目前为止,这仍然是我最好的想法。 - prom85

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接