RecyclerView头部和底部

66

也许这个问题以前已经被问过,但我似乎找不到确切的答案或解决方案。 我开始使用RecyclerView,并使用LinearLayoutManager实现它。 现在我想添加自定义标题和页脚项,它们与我的RecyclerView中的其他项目不同。 标题和页脚不应该是粘性的,我希望它们随着其他项目一起滚动。 有人能指出如何做到这一点的示例或共享想法吗? 我会非常感激。谢谢


@androiddeveloper 所以使用Spans。 - pskink
@pskink,我不明白。我提供的链接也处理跨度,是吗?你有不同的想法吗? - android developer
@androiddeveloper 如果你的网格中有3列,那么在位置0处返回3个getSpanSize,否则返回1。 - pskink
@pskink 是的。但它仍然不像 ListViews 上的标题那样,对吧?如果您希望添加 swipeToRefresh,但它应该显示在标题本身下面(就像 Google Now 启动器上一样),会发生什么?这仍然是可能的吗? - android developer
@pskink 打开Google Now Launcher,然后向左滑动(在主屏幕上)。现在,向上滑动以刷新。您会发现出现的进度条将显示在标题下方。 - android developer
显示剩余12条评论
12个回答

56

在你的适配器中添加这个类:

private class VIEW_TYPES {
        public static final int Header = 1;
        public static final int Normal = 2;
        public static final int Footer = 3;
}

然后像这样覆盖以下方法:

@Override
public int getItemViewType(int position) {

    if(items.get(position).isHeader)
        return VIEW_TYPES.Header;
    else if(items.get(position).isFooter)
        return VIEW_TYPES.Footer;
    else
        return VIEW_TYPES.Normal;

}

现在在onCreateViewHolder方法中,根据视图类型填充您的布局:

现在在onCreateViewHolder方法中,根据视图类型填充您的布局:

@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {

    View rowView;

    switch (i) {

        case VIEW_TYPES.Normal:
            rowView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.normal, viewGroup, false);
            break;
        case VIEW_TYPES.Header:
            rowView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.header, viewGroup, false);
            break;
        case VIEW_TYPES.Footer:
            rowView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.footer, viewGroup, false);
            break;
        default:
            rowView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.normal, viewGroup, false);
            break;
    }
    return new ViewHolder (rowView);
}

现在在onBindViewHolder方法中,根据视图持有者绑定您的布局:

@Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {

        int viewType = getItemViewType(position);

        switch(viewType) {

            case VIEW_TYPES.Header: // handle row header
                break;
            case VIEW_TYPES.Footer: // handle row footer
                break;
            case VIEW_TYPES.Normal: // handle row item
                break;

        }

    }

希望这可以帮到你。


我能够成功实现页眉,但当我添加页脚时,它会被插入到 Recycler 视图的第一个项目之后,尽管我已将 Footer 属性指定为“layout_alignParentBottom=true”。有任何想法是什么原因吗? - AndroidDev
2
嗨,页眉和页脚也是项目,因此您必须先添加页眉(检查它是否在第一位置),然后添加您的项目,最后在最后位置添加页脚。 - Bronx
谢谢你提供的解决方案,但是你能告诉我方法名"isHeader"和"isFooter"在哪里吗?@Bronx - Ronak Joshi
嗨@RonakJoshi,isHeader和isFooter不是方法,它们是您列表项中的变量。例如,您有一个名为MyItem的类,构造函数为public MyItem(Object myObject,boolean isHeader,boolean isFooter)。 - Bronx
1
@Bronx 或者添加完整代码,因为我仍然遇到了 santafebound 遇到的相同问题。 - X09
显示剩余5条评论

32

使用ItemDecorations非常容易,而且无需修改任何其他代码:

recyclerView.addItemDecoration(new HeaderDecoration(this,
                               recyclerView,  R.layout.test_header));

预留绘图的空间,填充你想要绘制的布局并在预留的空间中进行绘制。

Decoration的代码:

public class HeaderDecoration extends RecyclerView.ItemDecoration {

    private View mLayout;

    public HeaderDecoration(final Context context, RecyclerView parent, @LayoutRes int resId) {
        // inflate and measure the layout
        mLayout = LayoutInflater.from(context).inflate(resId, parent, false);
        mLayout.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
    }


    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        // layout basically just gets drawn on the reserved space on top of the first view
        mLayout.layout(parent.getLeft(), 0, parent.getRight(), mLayout.getMeasuredHeight());
        for (int i = 0; i < parent.getChildCount(); i++) {
            View view = parent.getChildAt(i);
            if (parent.getChildAdapterPosition(view) == 0) {
                c.save();
                final int height = mLayout.getMeasuredHeight();
                final int top = view.getTop() - height;
                c.translate(0, top);
                mLayout.draw(c);
                c.restore();
                break;
            }
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (parent.getChildAdapterPosition(view) == 0) {
            outRect.set(0, mLayout.getMeasuredHeight(), 0, 0);
        } else {
            outRect.setEmpty();
        }
    }
}

1
你如何在item decorator中添加点击处理?我必须添加一个按钮作为标题,通过你的代码可以看到它,但是我无法添加点击事件,因为findViewById返回null。 - chin87
1
@chin87...由于mLayout是一个视图 - 只需将其添加到视图中即可?可以添加一个getter或修改构造函数。(还要看你在哪里调用findViewById...因为该视图既没有附加到recyclerView也没有附加到片段中) - David Medenjak
2
你如何使用它来添加页脚?当LinearLayoutManager是水平的时,它是否也适用? - android developer
我使用TextView作为页脚布局,它不会换行,只会绘制单行文本视图,超出边界。 - Leo DroidCoder
1
如果有人正在寻找页脚版本,请访问以下链接:https://gist.github.com/dreiklangdev/4b092eade09feb26bdf1c090fd2e5643 - lenhuy2106
显示剩余14条评论

11

如果您只需要一个空白的页眉和页脚,以下是一种非常简单的方法(使用 Kotlin 编写):

class HeaderFooterDecoration(private val headerHeight: Int, private val footerHeight: Int) : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val adapter = parent.adapter ?: return
        when (parent.getChildAdapterPosition(view)) {
            0 -> outRect.top = headerHeight
            adapter.itemCount - 1 -> outRect.bottom = footerHeight
            else -> outRect.set(0, 0, 0, 0)
        }
    }
}

这样称呼它:

recyclerView.addItemDecoration(HeaderFooterDecoration(headerHeightPx, footerHeightPx))

1
很少能在这里找到最佳解决方案。简短、简洁、易读,完成工作。满分Sean :D - AMAN77
一个非常好的示例,附带源代码,用于创建带有页眉和页脚的RecyclerView https://www.loopwiki.com/ui-ux-design/recyclerview-with-header-and-footer-android-example/ - Amar Yadav

9
你可以使用这个GitHub库,以最简单的方式为你的RecyclerView添加页眉或页脚。
你需要在项目中添加HFRecyclerView库,或者你也可以通过Gradle来获取它。
compile 'com.mikhaellopez:hfrecyclerview:1.0.0'

此库基于 @hister 的工作

这是一个图像结果:

预览


9

recyclerview:1.2.0引入了ConcatAdapter

ConcatAdapter是一种新的RecyclerView适配器,可以线性地组合多个适配器。

如何使用ConcatAdapter?

将以下依赖项添加到您的build.gradle文件中

androidx.recyclerview:recyclerview:1.2.0-alpha04

如果你有多个适配器,你可以轻松地使用合并它们:

  MyAdapter adapter1 = ...;
 AnotherAdapter adapter2 = ...;
 ConcatAdapter merged = new ConcatAdapter(adapter1, adapter2);
 recyclerView.setAdapter(merged);

对于上面的示例,ConcatAdapter将呈现adapter1中的项,然后是adapter2中的项。
这里,您可以找到完整的文档。
这里找到完整的工作示例。
阅读这篇文章以获取更多信息。
这里,您可以找到源代码。

3
我建议不要定制rv适配器。
保持原样...在您的rv项目布局中,只需添加带有布局的页脚,并将其可见性设置为gone。
然后当您到达适配器中的最后一个项目时...将其设置为可见。
当您尝试此操作时,请确保将其添加到您的rv适配器中。
   @Override
    public void onBindViewHolder(final PersonViewHolder personViewHolder, int i) {
           if(i==List.size()) // Last item in recycle view
           personViewHolder.tv_footer.setVisibility(VISIBLE);// Make footer visible now }

 @Override
    public int getItemViewType(int position) {
        return position;
    }

对于Header也做同样的操作。 这里i==0,代表列表中的第一个项目。

对我来说,这是最简单的解决方案。


好的,你的方法很直接。但是,将视图设置为 View.GONE 大部分时间不会浪费内存吗? - Kathir
@Kathir...只有在屏幕区域可见时才会处理它...其他时间它被视为通常的记录数组并保留在后台,以便显示。 - Raktim Bhattacharya
谢谢@Raktim。我找到了一个相关的问题回答。简而言之,与没有该视图时消耗的内存相比,它仍然会占用内存,但比View.INVISIBLE更好。此外,请阅读此处以了解何时使用View.INVISIBLE - Kathir

3

这里是RecyclerView的一些标题项修饰

通过一些修改,你可以将其变成页脚

public class HeaderItemDecoration extends RecyclerView.ItemDecoration {

private View customView;

public HeaderItemDecoration(View view) {
    this.customView = view;
}

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDraw(c, parent, state);
    customView.layout(parent.getLeft(), 0, parent.getRight(), customView.getMeasuredHeight());
    for (int i = 0; i < parent.getChildCount(); i++) {
        View view = parent.getChildAt(i);
        if (parent.getChildAdapterPosition(view) == 0) {
            c.save();
            final int height = customView.getMeasuredHeight();
            final int top = view.getTop() - height;
            c.translate(0, top);
            customView.draw(c);
            c.restore();
            break;
        }
    }
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    if (parent.getChildAdapterPosition(view) == 0) {
        customView.measure(View.MeasureSpec.makeMeasureSpec(parent.getMeasuredWidth(), View.MeasureSpec.AT_MOST),
                View.MeasureSpec.makeMeasureSpec(parent.getMeasuredHeight(), View.MeasureSpec.AT_MOST));
        outRect.set(0, customView.getMeasuredHeight(), 0, 0);
    } else {
        outRect.setEmpty();
    }
}
}      

你的代码非常方便,但我尝试将其修改为页脚,通过更改 parent.getChildAdapterPosition(view) == 0 到 parent.getChildAdapterPosition(view) == parent.getAdapter().getItemCount() 但它没有起作用。嗯,在其他任何位置都可以工作,但不是最后一个位置,你能建议一些更改吗? - Anurag Bhalekar
非常感谢@Trunks!在我的情况下,我已经添加了一个简单的标题,只包含一个TextView,方法如下:recyclerView.addItemDecoration(new HeaderItemDecoration(aTextView), 0) - Umberto Covino
1
@AnuragBhalekar 父类.getChildAdapterPosition(view) == (父类.getAdapter().getItemCount()-1) - Driss Bounouar

2

另一种方法是将标题和RecyclerView包装在一个CoordinatorLayout中:

<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:elevation="0dp">

    <View
        android:id="@+id/header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_scrollFlags="scroll" />

</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />


我还没有找到创建页脚的方法。 - lenhuy2106

2

2

点击这里。 我扩展了RecyclerView.Adapter,使添加页眉和页脚变得更加容易。

class HFAdapter extends HFRecyclerViewAdapter<String, HFAdapter.DataViewHolder>{

    public HFAdapter(Context context) {
        super(context);
    }

    @Override
    public DataViewHolder onCreateDataItemViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.data_item, parent, false);
        return new DataViewHolder(v);
    }

    @Override
    public void onBindDataItemViewHolder(DataViewHolder holder, int position) {
        holder.itemTv.setText(getData().get(position));
    }

    class DataViewHolder extends RecyclerView.ViewHolder{
        TextView itemTv;
        public DataViewHolder(View itemView) {
            super(itemView);
            itemTv = (TextView)itemView.findViewById(R.id.itemTv);
        }
    }
}

//add header
View headerView = LayoutInflater.from(this).inflate(R.layout.header, recyclerView, false);
hfAdapter.setHeaderView(headerView);
//add footer
View footerView = LayoutInflater.from(this).inflate(R.layout.footer, recyclerView, false);
hfAdapter.setFooterView(footerView);

//remove
hfAdapter.removeHeader();
hfAdapter.removeFooter();

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