Android 给RecyclerView添加头部和尾部

之前我在GitHub上开源了一个可以实现RecyclerView列表分组的通用Adapter: GroupedRecyclerViewAdapter。也在博客上写了一篇专门先容它的实现和使用的文章:《Android 可分组的RecyclerViewAdapter》。有一些朋友在看了我的博文和使用我的开源库后,会主动的给我反馈一些问题和提出一些建议。我很高兴,这使我觉得我所做的这些不仅可以提升自己,同时也能帮助到别人(虽然我很少写博客和开源东西)。前几天就有朋友向我反馈,说他不仅需要一个分组的列表,同时还希翼能给列表添加头部和尾部。其实GroupedRecyclerViewAdapter的每个分组都是可以设置头部和尾部的。如果只是给一个普通的列表添加头部和尾部,只需要用GroupedRecyclerViewAdapter实现只有一个分组的列表就可以。但是他希翼的是可以实现多个分组的同时,给整个大列表也设置头部和尾部,这样的需求是我以前从来没有想过的,但我还是给出了自己的建议:列表的第一个分组只要头部,不要尾部和子项,把它当做整个大列表的头部,尾部的实现也一样。这样也能实现他的需求,但就是处理逻辑复杂了一点。其实我在设计GroupedRecyclerViewAdapter的时候,为了能让它有更好的扩展性和能方便实现更多的复杂布局,所以给它的头部、尾部和子项都支撑了多种类型的ViewTtype,有兴趣的朋友欢迎去看一下。

ListView添加头部和尾部的实现原理

在大家以前使用ListView的时候,ListView为大家提供了添加头部(addHeaderView())和尾部(addFooterView())的方法,让大家可以很方便的给列表添加头部和尾部,而且头部和尾部跟大家自己的ListView Adapter完全没有任何关系。那么ListView是如何做到的呢。让大家一起打开ListView的源码,看一下它是如何给列表添加头部(addHeaderView())的。(添加尾部的原理是一样的,这里就不单独说了)

    public void addHeaderView(View v) {
        addHeaderView(v, null, true);
    }

    public void addHeaderView(View v, Object data, boolean isSelectable) {
        final FixedViewInfo info = new FixedViewInfo();
        info.view = v;
        info.data = data;
        info.isSelectable = isSelectable;
        mHeaderViewInfos.add(info);
        mAreAllItemsSelectable &= isSelectable;

        // 将ListView的Adapter包装成HeaderViewListAdapter。
        if (mAdapter != null) {
            if (!(mAdapter instanceof HeaderViewListAdapter)) {
                mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }

            if (mDataSetObserver != null) {
                mDataSetObserver.onChanged();
            }
        }
    }

上面就是ListView添加头部的方法的源码,其中它会判断当前ListView的Adapter是不是HeaderViewListAdapter,如果不是,这把当前的Adapter包装成HeaderViewListAdapter。所以HeaderViewListAdapter就是实现ListView添加头部的关键,那么大家就去看一下HeaderViewListAdapter到底是个什么东西。

    public class HeaderViewListAdapter implements WrapperListAdapter, Filterable{
}

HeaderViewListAdapter实现了WrapperListAdapter接口,而WrapperListAdapter接口是ListAdapter的子接口,所以HeaderViewListAdapter就是ListAdapter的一个实现类,跟普通的ListView Adapter没有太大区别。HeaderViewListAdapter接收一个普通的ListView Adapter和ListView头部列表和尾部列表,并且对它们统一管理。下面分析一下它的核心代码。

    //构造方法:接收从外部传进来的头部列表信息和尾部列表信息,还有被包装的普通ListAdapter。
    public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
                                 ArrayList<ListView.FixedViewInfo> footerViewInfos,
                                 ListAdapter adapter) {
        
    }

    //返回整个列表的item个数,其实就是普通Adapter的item个数加上头尾部的个数。
    public int getCount() {
        if (mAdapter != null) {
            return getFootersCount() + getHeadersCount() + mAdapter.getCount();
        } else {
            return getFootersCount() + getHeadersCount();
        }
    }

    /返回当前列表项的ViewType。
    public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (mAdapter != null && position >= numHeaders) {
            int adjPosition = position - numHeaders;
            int adapterCount = mAdapter.getCount();
            //如果当前列表项是普通的列表项,则交由mAdapter处理。
            //传给mAdapter的position需要除掉头尾部的处理。
            if (adjPosition < adapterCount) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }
        //返回当前列表项时头部或者尾部
        return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        //如果当前列表项是头部,则返回对应的头部布局。
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).view;
        }

        //如果当前列表项是普通的列表项,则交由mAdapter处理。
        //传给mAdapter的position需要除掉头尾部的处理。
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getView(adjPosition, convertView, parent);
            }
        }

        //如果当前列表项是尾部,则返回对应的尾部布局。
        return mFooterViewInfos.get(adjPosition - adapterCount).view;
    }

这就是HeaderViewListAdapter的核心代码,大家可以看到,它的代码非常的简单,就是在getCount()、getItemViewType()、getView()中做了一些处理。在getCount()中它返回的个数里加上了头部和尾部的个数。在getItemViewType()、getView()中,它判断如果当前列表项是头部或者尾部的时候自己处理,否则就交由被包装的普通Adapter处理。所以HeaderViewListAdapter的主要作用就是管理ListView的头部和尾部的。

HeaderViewListAdapter的设计非常的巧妙,只需要把大家设置给ListView的Adapter包装一下,就可以让大家的ListView具有了添加头部和尾部的功能,而且丝毫不会影响到大家原来的Adapter。甚至于大家根本就不知道在大家给ListView添加头部的时候,ListView已经将大家原来的Adapter包装成HeaderViewListAdapter,大家也无需关心他的实现逻辑。

根据HeaderViewListAdapter的设计思路,大家是不是也可以给大家的RecyclerView.Adapter实现一个包装类,只要对大家自己的Adapter包装一下,就可以让大家的列表具有了添加头部和尾部的功能呢?带着这样的想法,于是我就自己动手写了一个专门用来包装RecyclerView的Adapter的包装类:HeaderViewAdapter。

HeaderViewAdapter的代码实现

HeaderViewAdapter的设计思路和实现的效果跟HeaderViewListAdapter是完全一样的,在代码的实现上会有所不同,毕竟ListView的Adapter和RecyclerView的Adapter是完全不同的两个东西。

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

    //被包装的Adapter。
    private RecyclerView.Adapter mAdapter;

    //用于存放HeaderView
    private final List<FixedViewInfo> mHeaderViewInfos = new ArrayList<>();

    //用于存放FooterView
    private final List<FixedViewInfo> mFooterViewInfos = new ArrayList<>();

    //用于监听被包装的Adapter的数据变化的监听器。它将被包装的Adapter的数据变化映射成HeaderViewAdapter的变化。
    private RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
                //这里是具体的代码实现,因为篇幅的关系,在这里就不放出来了。
    };

    public HeaderViewAdapter(RecyclerView.Adapter adapter) {
        this.mAdapter = adapter;
        if (mAdapter != null) {
            //注册mAdapter的数据变化监听
            mAdapter.registerAdapterDataObserver(mObserver);
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 根据viewType查找对应的HeaderView 或 FooterView。如果没有找到则表示该viewType是普通的列表项。
        View view = findViewForInfos(viewType);
        if (view != null) {
            return new ViewHolder(view);
        } else {
            //交由mAdapter处理。
            return mAdapter.onCreateViewHolder(parent, viewType);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        // 如果是HeaderView 或者是 FooterView则不绑定数据。
        // 因为HeaderView和FooterView是由外部传进来的,它们不由列表去更新。
        if (isHeader(position) || isFooter(position)) {
            return;
        }

        //将列表实际的position调整成mAdapter对应的position。
        //交由mAdapter处理。
        int adjPosition = position - getHeadersCount();
        mAdapter.onBindViewHolder(holder, adjPosition);
    }

    @Override
    public int getItemCount() {
        return mHeaderViewInfos.size() + mFooterViewInfos.size()
                + (mAdapter == null ? 0 : mAdapter.getItemCount());
    }

    @Override
    public int getItemViewType(int position) {
        //如果当前item是HeaderView,则返回HeaderView对应的itemViewType。
        if (isHeader(position)) {
            return mHeaderViewInfos.get(position).itemViewType;
        }

        //如果当前item是HeaderView,则返回HeaderView对应的itemViewType。
        if (isFooter(position)) {
            return mFooterViewInfos.get(position - mHeaderViewInfos.size() - mAdapter.getItemCount()).itemViewType;
        }

        //将列表实际的position调整成mAdapter对应的position。
        //交由mAdapter处理。
        int adjPosition = position - getHeadersCount();
        return mAdapter.getItemViewType(adjPosition);
    }

    /**
     * 判断当前位置是否是头部View。
     *
     * @param position 这里的position是整个列表(包含HeaderView和FooterView)的position。
     * @return
     */
    public boolean isHeader(int position) {
        return position < getHeadersCount();
    }

    /**
     * 判断当前位置是否是尾部View。
     *
     * @param position 这里的position是整个列表(包含HeaderView和FooterView)的position。
     * @return
     */
    public boolean isFooter(int position) {
        return getItemCount() - position <= getFootersCount();
    }

    /**
     * 获取HeaderView的个数
     *
     * @return
     */
    public int getHeadersCount() {
        return mHeaderViewInfos.size();
    }

    /**
     * 获取FooterView的个数
     *
     * @return
     */
    public int getFootersCount() {
        return mFooterViewInfos.size();
    }

    /**
     * 添加HeaderView
     *
     * @param view
     */
    public void addHeaderView(View view) {
        addHeaderView(view, generateUniqueViewType());
    }

    private void addHeaderView(View view, int viewType) {
        //包装HeaderView数据并添加到列表
        FixedViewInfo info = new FixedViewInfo();
        info.view = view;
        info.itemViewType = viewType;
        mHeaderViewInfos.add(info);
        notifyDataSetChanged();
    }

    /**
     * 添加FooterView
     *
     * @param view
     */
    public void addFooterView(View view) {
        addFooterView(view, generateUniqueViewType());
    }

    private void addFooterView(View view, int viewType) {
        // 包装FooterView数据并添加到列表
        FixedViewInfo info = new FixedViewInfo();
        info.view = view;
        info.itemViewType = viewType;
        mFooterViewInfos.add(info);
        notifyDataSetChanged();
    }

    /**
     * 生成一个唯一的数,用于标识HeaderView或FooterView的type类型,并且保证类型不会重复。
     *
     * @return
     */
    private int generateUniqueViewType() {
        int count = getItemCount();
        while (true) {
            //生成一个随机数。
            int viewType = (int) (Math.random() * Integer.MAX_VALUE) + 1;

            //判断该viewType是否已使用。
            boolean isExist = false;
            for (int i = 0; i < count; i++) {
                if (viewType == getItemViewType(i)) {
                    isExist = true;
                    break;
                }
            }

            //判断该viewType还没被使用,则返回。否则进行下一次循环,重新生成随机数。
            if (!isExist) {
                return viewType;
            }
        }
    }

    /**
     * 根据viewType查找对应的HeaderView 或 FooterView。没有找到则返回null。
     *
     * @param viewType 查找的viewType
     * @return
     */
    private View findViewForInfos(int viewType) {
        for (FixedViewInfo info : mHeaderViewInfos) {
            if (info.itemViewType == viewType) {
                return info.view;
            }
        }

        for (FixedViewInfo info : mFooterViewInfos) {
            if (info.itemViewType == viewType) {
                return info.view;
            }
        }

        return null;
    }

    /**
     * 用于包装HeaderView和FooterView的数据类
     */
    private class FixedViewInfo {
        //保存HeaderView或FooterView
        View view;

        //保存HeaderView或FooterView对应的viewType。
        int itemViewType;
    }

    private static class ViewHolder extends RecyclerView.ViewHolder {
        ViewHolder(View itemView) {
            super(itemView);
        }
    }
}

为了让大家看代码方便一点,我删除了一些不是很重要的代码,而且把代码的实现细节讲解写到了每个方法的注释上,相信大家都能很容易的看懂。如果大家想看完整的代码,请移步我的GitHub。

HeaderViewAdapter的使用

我已经把HeaderViewAdapter和相关的类打包成一个引用库放到GitHub上,欢迎大家使用和star。

在ListView的设计上,对HeaderViewListAdapter的所有操作的是由ListView自己完成的。但是大家无法把对HeaderViewAdapter的操作交由RecyclerView来处理,所以需要自己对HeaderViewAdapter进行操作(包装、添加头部和尾部等),其实这也很简单,几句代码的事情而已。而且它可以适用于任何的RecyclerView,没有任何使用上的限制。

    //需要包装的adapter
    LinearAdapter adapter = new LinearAdapter(this);
    
    //对adapter进行包装。
    HeaderViewAdapter headerViewAdapter = new HeaderViewAdapter(adapter);
    
    //添加HeaderView和FooterView
    headerViewAdapter.addHeaderView(headerView);
    headerViewAdapter.addFooterView(footerView);
    
    //设置Adapter
    recyclerView.setAdapter(headerViewAdapter);

无论大家的RecyclerView使用什么LayoutManager,HeaderViewAdapter都需要保证列表的头部和尾部能占满一行,否则布局就会很难看。使用LinearLayoutManager的时候不需要做特殊的处理,HeaderViewAdapter也已经帮大家处理了StaggeredGridLayoutManager的情况。至于GridLayoutManager的情况,我在HeaderViewAdapter的库里提供了一个HeaderViewGridLayoutManager的子类。所以大家在使用GridLayoutManager的时候,应该使用HeaderViewGridLayoutManager。

    recyclerView.setLayoutManager(new HeaderViewGridLayoutManager(this, 2, headerViewAdapter));

为了让大家的RecyclerView添加头部和尾部的时候,更接近于ListView的体验。所以我在库里提供了一个RecyclerView子类:HeaderRecyclerView。HeaderRecyclerView封装了对HeaderViewAdapter的所以操作,这使大家只需要操作HeaderRecyclerView,而无需直接跟HeaderViewAdapter打交道,这使得大家使用HeaderRecyclerView的时候就如同以前使用ListView一样。

    HeaderRecyclerView rvList = (HeaderRecyclerView) findViewById(R.id.rv_list);
    //这是普通的adapter
    GridAdapter adapter = new GridAdapter(this);
    rvList.setLayoutManager(new GridLayoutManager(this, 2));
    //直接设置普通的adapter,不需要直接进行包装。
    rvList.setAdapter(adapter);

    //添加HeaderView和FooterView。直接操作HeaderRecyclerView。
    rvList.addHeaderView(headerView);
    rvList.addFooterView(footerView);

效果图:

LinearList.gif

GridList.gif

传送门:https://github.com/donkingliang/HeaderViewAdapter

推荐阅读更多精彩内容