Android 5.0 - 将页眉/页脚添加到RecyclerView

Mat*_*ree 120 java android android-5.0-lollipop android-recyclerview

我花了一些时间试图找出一种方法来添加一个标题RecyclerView,但没有成功.这是我到目前为止所得到的:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    layouManager = new LinearLayoutManager(getActivity());
    recyclerView.setLayoutManager(layouManager);

    LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    headerPlaceHolder = inflater.inflate(R.layout.view_header_holder_medium, null, false);
    layouManager.addView(headerPlaceHolder, 0);

   ...
}
Run Code Online (Sandbox Code Playgroud)

LayoutManager似乎是处理的配置对象RecyclerView的项目.因为我无法找到任何addHeaderView(View view)方法,我决定去与LayoutManageraddView(View view, int position)方法,并增加我的头鉴于第一位置,像一个头.

Aaand这是事情变得更加丑陋的地方:

java.lang.NullPointerException: Attempt to read from field 'android.support.v7.widget.RecyclerView$ViewHolder android.support.v7.widget.RecyclerView$LayoutParams.mViewHolder' on a null object reference
    at android.support.v7.widget.RecyclerView.getChildViewHolderInt(RecyclerView.java:2497)
    at android.support.v7.widget.RecyclerView$LayoutManager.addViewInt(RecyclerView.java:4807)
    at android.support.v7.widget.RecyclerView$LayoutManager.addView(RecyclerView.java:4803)
    at com.mathieumaree.showz.fragments.CategoryFragment.setRecyclerView(CategoryFragment.java:231)
    at com.mathieumaree.showz.fragments.CategoryFragment.access$200(CategoryFragment.java:47)
    at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:201)
    at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:196)
    at retrofit.CallbackRunnable$1.run(CallbackRunnable.java:41)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:135)
    at android.app.ActivityThread.main(ActivityThread.java:5221)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Run Code Online (Sandbox Code Playgroud)

在几次NullPointerExceptions尝试调用addView(View view)Activity创建的不同时刻(也尝试在设置完一切后添加视图,甚至是Adapter的数据)后,我意识到我不知道这是否是正确的方法(并且它看起来不是).

PS:此外,一个可以处理GridLayoutManager除此之外的解决方案LinearLayoutManager将非常感谢!

Rea*_*hed 108

找到关于此https://plus.google.com/+WillBlaschko/posts/3MFmgPbQuWx的非常好的文章

我不得不为我添加一个页脚,我RecyclerView在这里分享我的代码片段,因为我觉得它可能有用.请检查代码中的注释,以便更好地了解整体流程.

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

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

    private static final int FOOTER_VIEW = 1;
    private ArrayList<String> data; // Take any list that matches your requirement.
    private Context context;

    // Define a constructor
    public RecyclerViewWithFooterAdapter(Context context, ArrayList<String> data) {
        this.context = context;
        this.data = data;
    }

    // Define a ViewHolder for Footer view
    public class FooterViewHolder extends ViewHolder {
        public FooterViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the item
                }
            });
        }
    }

    // Now define the ViewHolder for Normal list item
    public class NormalViewHolder extends ViewHolder {
        public NormalViewHolder(View itemView) {
            super(itemView);

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the normal items
                }
            });
        }
    }

    // And now in onCreateViewHolder you have to pass the correct view
    // while populating the list item.

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View v;

        if (viewType == FOOTER_VIEW) {
            v = LayoutInflater.from(context).inflate(R.layout.list_item_footer, parent, false);
            FooterViewHolder vh = new FooterViewHolder(v);
            return vh;
        }

        v = LayoutInflater.from(context).inflate(R.layout.list_item_normal, parent, false);

        NormalViewHolder vh = new NormalViewHolder(v);

        return vh;
    }

    // Now bind the ViewHolder in onBindViewHolder
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        try {
            if (holder instanceof NormalViewHolder) {
                NormalViewHolder vh = (NormalViewHolder) holder;

                vh.bindView(position);
            } else if (holder instanceof FooterViewHolder) {
                FooterViewHolder vh = (FooterViewHolder) holder;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Now the critical part. You have return the exact item count of your list
    // I've only one footer. So I returned data.size() + 1
    // If you've multiple headers and footers, you've to return total count
    // like, headers.size() + data.size() + footers.size()

    @Override
    public int getItemCount() {
        if (data == null) {
            return 0;
        }

        if (data.size() == 0) {
            //Return 1 here to show nothing
            return 1;
        }

        // Add extra view to show the footer view
        return data.size() + 1;
    }

    // Now define getItemViewType of your own.

    @Override
    public int getItemViewType(int position) {
        if (position == data.size()) {
            // This is where we'll add footer.
            return FOOTER_VIEW;
        }

        return super.getItemViewType(position);
    }

    // So you're done with adding a footer and its action on onClick.
    // Now set the default ViewHolder for NormalViewHolder

    public class ViewHolder extends RecyclerView.ViewHolder {
        // Define elements of a row here
        public ViewHolder(View itemView) {
            super(itemView);
            // Find view by ID and initialize here
        }

        public void bindView(int position) {
            // bindView() method to implement actions
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的代码片段添加了一个页脚RecyclerView.您可以检查此GitHub存储库以检查添加页眉和页脚的实现.

  • 人们总是想做很多手工工作。我不敢相信... (8认同)
  • 效果很好.这应该标记为正确答案. (2认同)
  • `int getItemViewType (int position)` - 出于视图回收的目的,返回位置处项目的视图类型。此方法的默认实现返回 0,假设适配器为单一视图类型。与 ListView 适配器不同,类型不需要是连续的。考虑使用 id 资源来唯一标识项目视图类型。- 从文档。https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html (2认同)

Nis*_*hah 29

很简单解决!!

我不喜欢在适配器中将逻辑作为不同的视图类型,因为每次它在返回视图之前检查视图类型.以下解决方案避免额外检查.

只需在android.support.v4.widget.NestedScrollView中添加LinearLayout(垂直)标题视图+ recyclerview + footer视图即可.

看一下这个:

 <android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

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

        <android.support.v7.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layoutManager="LinearLayoutManager"/>

        <View
            android:id="@+id/footer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>
Run Code Online (Sandbox Code Playgroud)

添加此行代码以实现平滑滚动

RecyclerView v = (RecyclerView) findViewById(...);
v.setNestedScrollingEnabled(false);
Run Code Online (Sandbox Code Playgroud)

这将失去所有RV性能,RV将尝试布置所有视图持有者,无论layout_heightRV

  • 这是一种非常简单的方法,它失去了"RecyclerView"带来的所有优势 - 你失去了实际的回收利用和它带来的优化. (26认同)
  • 使用嵌套的scrollview将导致recyclerview缓存所有视图,并且如果recycler视图的大小更大,则滚动并且加载时间将增加。我建议不要使用此代码 (2认同)

seb*_*seb 25

我在Lollipop上遇到了同样的问题,并创建了两种方法来包装Recyclerview适配器.一个很容易使用,但我不确定它将如何与不断变化的数据集一起使用.因为它包装了你的适配器,你需要确保调用类似于notifyDataSetChanged右侧适配器对象的方法.

另一个不应该有这样的问题.让你的常规适配器扩展类,实现抽象方法,你应该准备好了.他们在这里:

学家

HeaderRecyclerViewAdapterV1

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * Created by sebnapi on 08.11.14.
 * <p/>
 * This is a Plug-and-Play Approach for adding a Header or Footer to
 * a RecyclerView backed list
 * <p/>
 * Just wrap your regular adapter like this
 * <p/>
 * new HeaderRecyclerViewAdapterV1(new RegularAdapter())
 * <p/>
 * Let RegularAdapter implement HeaderRecyclerView, FooterRecyclerView or both
 * and you are ready to go.
 * <p/>
 * I'm absolutely not sure how this will behave with changes in the dataset.
 * You can always wrap a fresh adapter and make sure to not change the old one or
 * use my other approach.
 * <p/>
 * With the other approach you need to let your Adapter extend HeaderRecyclerViewAdapterV2
 * (and therefore change potentially more code) but possible omit these shortcomings.
 * <p/>
 * TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
 */
public class HeaderRecyclerViewAdapterV1 extends RecyclerView.Adapter {
    private static final int TYPE_HEADER = Integer.MIN_VALUE;
    private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
    private static final int TYPE_ADAPTEE_OFFSET = 2;

    private final RecyclerView.Adapter mAdaptee;


    public HeaderRecyclerViewAdapterV1(RecyclerView.Adapter adaptee) {
        mAdaptee = adaptee;
    }

    public RecyclerView.Adapter getAdaptee() {
        return mAdaptee;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER && mAdaptee instanceof HeaderRecyclerView) {
            return ((HeaderRecyclerView) mAdaptee).onCreateHeaderViewHolder(parent, viewType);
        } else if (viewType == TYPE_FOOTER && mAdaptee instanceof FooterRecyclerView) {
            return ((FooterRecyclerView) mAdaptee).onCreateFooterViewHolder(parent, viewType);
        }
        return mAdaptee.onCreateViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && holder.getItemViewType() == TYPE_HEADER && useHeader()) {
            ((HeaderRecyclerView) mAdaptee).onBindHeaderView(holder, position);
        } else if (position == mAdaptee.getItemCount() && holder.getItemViewType() == TYPE_FOOTER && useFooter()) {
            ((FooterRecyclerView) mAdaptee).onBindFooterView(holder, position);
        } else {
            mAdaptee.onBindViewHolder(holder, position - (useHeader() ? 1 : 0));
        }
    }

    @Override
    public int getItemCount() {
        int itemCount = mAdaptee.getItemCount();
        if (useHeader()) {
            itemCount += 1;
        }
        if (useFooter()) {
            itemCount += 1;
        }
        return itemCount;
    }

    private boolean useHeader() {
        if (mAdaptee instanceof HeaderRecyclerView) {
            return true;
        }
        return false;
    }

    private boolean useFooter() {
        if (mAdaptee instanceof FooterRecyclerView) {
            return true;
        }
        return false;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && useHeader()) {
            return TYPE_HEADER;
        }
        if (position == mAdaptee.getItemCount() && useFooter()) {
            return TYPE_FOOTER;
        }
        if (mAdaptee.getItemCount() >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
            new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
        }
        return mAdaptee.getItemViewType(position) + TYPE_ADAPTEE_OFFSET;
    }


    public static interface HeaderRecyclerView {
        public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);

        public void onBindHeaderView(RecyclerView.ViewHolder holder, int position);
    }

    public static interface FooterRecyclerView {
        public RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);

        public void onBindFooterView(RecyclerView.ViewHolder holder, int position);
    }

}
Run Code Online (Sandbox Code Playgroud)

HeaderRecyclerViewAdapterV2

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * Created by sebnapi on 08.11.14.
 * <p/>
 * If you extend this Adapter you are able to add a Header, a Footer or both
 * by a similar ViewHolder pattern as in RecyclerView.
 * <p/>
 * If you want to omit changes to your class hierarchy you can try the Plug-and-Play
 * approach HeaderRecyclerViewAdapterV1.
 * <p/>
 * Don't override (Be careful while overriding)
 * - onCreateViewHolder
 * - onBindViewHolder
 * - getItemCount
 * - getItemViewType
 * <p/>
 * You need to override the abstract methods introduced by this class. This class
 * is not using generics as RecyclerView.Adapter make yourself sure to cast right.
 * <p/>
 * TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
 */
public abstract class HeaderRecyclerViewAdapterV2 extends RecyclerView.Adapter {
    private static final int TYPE_HEADER = Integer.MIN_VALUE;
    private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
    private static final int TYPE_ADAPTEE_OFFSET = 2;

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER) {
            return onCreateHeaderViewHolder(parent, viewType);
        } else if (viewType == TYPE_FOOTER) {
            return onCreateFooterViewHolder(parent, viewType);
        }
        return onCreateBasicItemViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && holder.getItemViewType() == TYPE_HEADER) {
            onBindHeaderView(holder, position);
        } else if (position == getBasicItemCount() && holder.getItemViewType() == TYPE_FOOTER) {
            onBindFooterView(holder, position);
        } else {
            onBindBasicItemView(holder, position - (useHeader() ? 1 : 0));
        }
    }

    @Override
    public int getItemCount() {
        int itemCount = getBasicItemCount();
        if (useHeader()) {
            itemCount += 1;
        }
        if (useFooter()) {
            itemCount += 1;
        }
        return itemCount;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && useHeader()) {
            return TYPE_HEADER;
        }
        if (position == getBasicItemCount() && useFooter()) {
            return TYPE_FOOTER;
        }
        if (getBasicItemType(position) >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
            new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
        }
        return getBasicItemType(position) + TYPE_ADAPTEE_OFFSET;
    }

    public abstract boolean useHeader();

    public abstract RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindHeaderView(RecyclerView.ViewHolder holder, int position);

    public abstract boolean useFooter();

    public abstract RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindFooterView(RecyclerView.ViewHolder holder, int position);

    public abstract RecyclerView.ViewHolder onCreateBasicItemViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindBasicItemView(RecyclerView.ViewHolder holder, int position);

    public abstract int getBasicItemCount();

    /**
     * make sure you don't use [Integer.MAX_VALUE-1, Integer.MAX_VALUE] as BasicItemViewType
     *
     * @param position
     * @return
     */
    public abstract int getBasicItemType(int position);

}
Run Code Online (Sandbox Code Playgroud)

反馈和叉子赞赏.我将自己使用HeaderRecyclerViewAdapterV2并进化,测试并发布未来的变化.

编辑:@OvidiuLatcu是的,我有一些问题.实际上我已经停止了隐式偏移Header position - (useHeader() ? 1 : 0),而是int offsetPosition(int position)为它创建了一个公共方法.因为如果您设置了一个OnItemTouchListenerRecyclerview,您可以截取触摸,获取触摸的x,y坐标,找到相应的子视图,然后调用recyclerView.getChildPosition(...),您将始终获得适配器中的非偏移位置!这是RecyclerView Code中的一个简短命令,我没有看到一个简单的方法来克服这个问题.这就是为什么我现在需要通过自己的代码来明确偏移位置.


Ian*_*son 10

我没有试过这个,但我只是将1(或2,如果你想要一个页眉和页脚)添加到你的适配器中的getItemCount返回的整数.然后getItemViewType,您可以在适配器中覆盖以在以下情况下返回不同的整数i==0:https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType( int )

createViewHolder然后传递您返回的整数getItemViewType,允许您以不同的方式为标题视图创建或配置视图持有者:https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html# createViewHolder(android.view.ViewGroup,int)

不要忘记从传递给的位置整数中减去一个bindViewHolder.


lop*_*ael 9

您可以使用此GitHub库,以最简单的方式在RecyclerView中添加页眉和/或页脚.

您需要在项目中添加HFRecyclerView库,或者您也可以从Gradle中获取它:

compile 'com.mikhaellopez:hfrecyclerview:1.0.0'
Run Code Online (Sandbox Code Playgroud)

这是图像的结果:

预习

编辑:

如果您只想在此库的顶部和/或底部添加边距:SimpleItemDecoration:

int offsetPx = 10;
recyclerView.addItemDecoration(new StartOffsetItemDecoration(offsetPx));
recyclerView.addItemDecoration(new EndOffsetItemDecoration(offsetPx));
Run Code Online (Sandbox Code Playgroud)


Val*_*kov 8

recyclerview:1.2.0引入了ConcatAdapter类,它将多个适配器连接成一个适配器。因此它允许创建单独的页眉/页脚适配器并在多个列表中重用它们。

myRecyclerView.adapter = ConcatAdapter(headerAdapter, listAdapter, footerAdapter)
Run Code Online (Sandbox Code Playgroud)

看看公告文章。它包含如何使用在页眉和页脚中显示加载进度的示例ConcatAdapter

目前,当我发布这个答案时,1.2.0库的版本处于 alpha 阶段,因此 api 可能会发生变化。您可以在此处查看状态。


dar*_*son 6

我最终实现了自己的适配器来包装任何其他适配器并提供添加页眉和页脚视图的方法.

在这里创建了一个要点:HeaderViewRecyclerAdapter.java

其主要的功能,我想要的是一个类似的界面到ListView,所以我希望能够膨胀的意见,我的片段,并将它们添加到RecyclerViewonCreateView.这是通过创建HeaderViewRecyclerAdapter传递要包装的适配器,并调用addHeaderViewaddFooterView传递膨胀的视图来完成的.然后将HeaderViewRecyclerAdapter实例设置为适配器RecyclerView.

一个额外的要求是我需要能够在保持页眉和页脚的同时轻松更换适配器,我不想让多个适配器具有这些页眉和页脚的多个实例.因此,您可以调用setAdapter更改已包装的适配器,使页眉和页脚保持完整,并RecyclerView通知更改.