美文网首页Android开发Android知识android 开发程序员
Android 给RecyclerView添加头部和尾部

Android 给RecyclerView添加头部和尾部

作者: donkingliang | 来源:发表于2017-11-13 14:36 被阅读106次

    之前我在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

    相关文章

      网友评论

      • 78983551cc90:不知道你有没有发现添加三个以上Header会抛出异常
        java.lang.IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true
        donkingliang:@设计师吴彦祖 如果你想添加多个一样的头部,应该是创建多个对象,而不是同一个对象添加多次。同一个对象是不能被添加多次的。
        View hv1 = LayoutInflater.from(this).inflate(R.layout.layout_header_view, rvList, false);
        View hv2 = LayoutInflater.from(this).inflate(R.layout.layout_header_view, rvList, false);
        headerViewAdapter.addHeaderView(hv1);
        headerViewAdapter.addHeaderView(hv2);
        78983551cc90:@donkingliang 是啊, 同一个头部多次添加
        donkingliang:不会啊,我同时添加很多个头部和尾部都不会有问题啊。你是不是把同一个头部View对象多次添加了啊?

      本文标题:Android 给RecyclerView添加头部和尾部

      本文链接:https://www.haomeiwen.com/subject/djdnmxtx.html