实现带header和footer功能的RecyclerView

Android进阶之路系列:http://blog.csdn.net/column/details/16488.html

这个项目很简单,其实一年前就开发完成了,但是一直没闲下来去整理。

RecyclerView是Android 5.0版本引入的一个新的组件,目的是在一些场景中取代之前ListView和GridView,实现性能更优的解决方案。同时RecyclerView的灵活性让它可胜任更多的场景。关于RecyclerView的使用有太多的文章了,大家可以自行搜索。

我们知道RecyclerView很灵活,灵活到很多功能需要我们自己实现,比如ListView和GridView中最常用的Item点击事件。所以在使用了几次后,我准备自己封装一个WrapRecyclerView,实现一些非常常用的功能。

在ListView中我们经常使用header和footer功能,确实也给我们带来了不少方便,而且使用场景很多。但是在RecyclerView并没有默认实现这个功能,所以WrapRecyclerView首要任务就是添加这个功能。

实现后的效果图如下:

实现带header和footer功能的RecyclerView_第1张图片
linear.png
实现带header和footer功能的RecyclerView_第2张图片
grid.png
实现带header和footer功能的RecyclerView_第3张图片
staggered.png

首先,我们为了WrapRecyclerView创建一个内部类 WrapAdapterextendsAdapter ,同时重写WrapRecyclerView部分方法,如

@Override  
public void setAdapter(Adapter adapter) {  
    mWrapAdapter.setAdapter(adapter);  
    super.setAdapter(mWrapAdapter);  
}  
  
@Override  
public Adapter getAdapter() {  
    return mWrapAdapter.getAdapter();  
}  

将传入的adapter(在下面内容中我们称这个adapter为外部adapter)交给WrapAdapter来处理,WrapAdapter在WrapRecyclerView构造函数中已经初始化。
在WrapAdapter中我们增加一些针对header和footer的方法,如

public void addHeaderView(View header){  
    if(mHeaderViews == null){  
        mHeaderViews = new ArrayList();  
    }  
    if(!mHeaderViews.contains(header)) {  
        mHeaderViews.add(header);  
    }  
    notifyDataSetChanged();  
}  
  
public void removeHeaderView(View header){  
    if(mHeaderViews != null){  
        mHeaderViews.remove(header);  
        notifyDataSetChanged();  
    }  
}  
  
public int getHeaderCount(){  
    if(mHeaderViews == null){  
        return 0;  
    }  
    return mHeaderViews.size();  
}  
  
public void addFooterView(View footer){  
    if(mFooterViews == null){  
        mFooterViews = new ArrayList();  
    }  
    if(!mFooterViews.contains(footer)) {  
        mFooterViews.add(footer);  
    }  
    notifyDataSetChanged();  
}  
  
public void removeFooterView(View footer){  
    if(mFooterViews != null){  
        mFooterViews.remove(footer);  
        notifyDataSetChanged();  
    }  
}  
  
public int getFooterCount(){  
    if(mFooterViews == null){  
        return 0;  
    }  
    return mFooterViews.size();  
}  

这部分代码比较简单,不详细解释了,主要是在WrapAdapter中分别用两个list(mHeaderViews和mFooterViews)来管理header和footer。

接下来要区分item做不同的处理,使用getItemViewType来区分不同的item,如

@Override  
public int getItemViewType(int position) {  
    if(position < getHeaderCount()){  
        return TYPE_HEADER - position;  
    }  
    else if(position < getItemCount() - getFooterCount()){  
        return mAdapter.getItemViewType(position - getHeaderCount());  
    }  
    else {  
        return TYPE_FOOTER - position + getItemCount() - getFooterCount();  
    }  
}  

TYPE_HEADER和TYPE_FOOTER分别为-1000和-2000(一般header和footer不会有太多)。
如果是正常的item,直接调用外部adapter的对应方法;如果是header和footer,在对应标识上要减去该header或footer在对应的list中的位置,下面就会解释这样做的原因。

得到了不同的type之后在create和bind就可以根据type做不同的处理,如

@Override  
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {  
    ViewHolder holder = null;  
    int type = 0;  
    int position = 0;  
    if(viewType <= TYPE_FOOTER){  
        type = TYPE_FOOTER;  
        position = TYPE_FOOTER - viewType;  
    }  
    else if(viewType <= TYPE_HEADER){  
        type = TYPE_HEADER;  
        position = TYPE_HEADER - viewType;  
    }  
    else{  
        type = viewType;  
    }  
    switch (type){  
        case TYPE_HEADER:  
            View header = mHeaderViews.get(position);  
            setWrapParems(header);  
            holder = new WrapViewHolder(header);  
            break;  
        case TYPE_FOOTER:  
            View footer = mFooterViews.get(position);  
            setWrapParems(footer);  
            holder = new WrapViewHolder(footer);  
            break;  
        default:  
            holder = mAdapter.onCreateViewHolder(parent, viewType);  
            break;  
    }  
    return holder;  
}  
  
@Override  
public void onBindViewHolder(ViewHolder holder, int position) {  
    if (holder instanceof WrapViewHolder){  
    }  
    else {  
        int realPosition = position - getHeaderCount();  
        mAdapter.onBindViewHolder(holder, realPosition);  
        holder.itemView.setOnClickListener(new OnPositionClick(realPosition));  
        holder.itemView.setOnLongClickListener(new OnPositionLongClick(realPosition));  
    }  
}  

在onCreateViewHolder中不仅要区分type,同时如果是header或footer还需要知道是哪一个,这就是前面代码中在type中添入位置的原因。
如果是item,直接调用外部adapter的create方法来生成view;如果是header或footer,则根据计算出来的position从list中获取并封装进一个WrapViewHolder。

在onBindViewHolder中判断如果是WrapViewHolder则表示是header或footer,一般header 和footer在添加进来之前数据都加载到view中了,这里不再处理;否则调用外部adapter的bind方法加载数据。

经过上面几步,我们已经构建了一个带有header和footer的adapter。但是还有一个问题,因为RecyclerView有三种LayoutManager:LinearLayoutManager、GridLayoutManager、StaggeredLayoutManager。对于LinearLayoutManager来说,上面封装的功能已经可以实现header和footer了。但是对于其他两个来说,还远远不够。
由于GridLayoutManager和StaggeredLayoutManager是多列的,每个header和footer都需要独占一行,所以我们需要对这两种LayoutManager分别作一些处理。

首先是GridLayoutManager,需要在WrapRecyclerView中重写setLayoutManager,代码如下

@Override  
public void setLayoutManager(final LayoutManager layout) {  
    if(layout instanceof GridLayoutManager){  
        final GridLayoutManager.SpanSizeLookup old = ((GridLayoutManager) layout).getSpanSizeLookup();  
        ((GridLayoutManager) layout).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {  
            @Override  
            public int getSpanSize(int position) {  
                if(mWrapAdapter.isHeader(position) || mWrapAdapter.isFooter(position)){  
                    return ((GridLayoutManager) layout).getSpanCount();  
                }  
                if(old != null){  
                    return old.getSpanSize(position - mWrapAdapter.getHeaderCount());  
                }  
                return 1;  
            }  
        });  
    }  
    super.setLayoutManager(layout);  
}  

当为WrapRecyclerView设置的LayoutManager是GridLayoutManager时,为其设置SpanSizeLookup,并通过position判断如果是header或footer返回SpanCount(这个count是初始化GridLayoutMananger设置的每行的列数),这样就保证了header和footer可以独占一行。
注意:这里考虑到用户也需要自定义SpanSizeLookup,所以在设置前先获取一下,如果存在则在getSpanSize中返回正确的值保证显示效果。

StaggeredLayoutManager则需要另外一种处理方法。在之前的onCreateViewHolder代码中可以看到存在setWrapParams方法,这个方法代码如下

private void setWrapParems(View view){  
    int width = getLayoutManager().canScrollVertically() ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;  
    int height = getLayoutManager().canScrollVertically() ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT;  
    if(getLayoutManager() instanceof StaggeredGridLayoutManager) {  
        StaggeredGridLayoutManager.LayoutParams headerParams = new StaggeredGridLayoutManager.LayoutParams(width, height);  
        headerParams.setFullSpan(true);  
        view.setLayoutParams(headerParams);  
    }  
}  

在如果LayoutManager是StaggeredLayoutManager,需要创建一个LayoutParams并设置FullSpan,赋予header或footer的view即可独占一行。
为了让header和footer功能适应横向和竖向,还需要判断设定的方向后为LayoutParams设置不同的宽和高。

本章的内容基本结束了,这个功能的关键点有两个:一个是对header和footer的管理,包括判断、创建、加载等环节;另外一个就是实现在GridLayoutManager和StaggeredLayoutManager中独占一行。

由于我们大量的重写了很多父类方法,所以一些关联的其他方法也需要整理逻辑重写,这里就不细说了,具体可以参考源码。

本项目的github地址是https://github.com/chzphoenix/PullToRefreshRecyclerView

下一章会完善header和footer功能,比如处理添加divider导致错乱的问题,敬请期待!!

Android进阶之路系列:http://blog.csdn.net/column/details/16488.html

你可能感兴趣的:(实现带header和footer功能的RecyclerView)