RecyclerView添加HeaderView和FooterView

前面已经说过RecyclerView的使用,RecyclerView通过其高度的可定制性深受大家的青睐,也有非常多的使用者开始对它进行封装或者改造,从而满足越来越多的需求,相信用过的一定会爱不释手。
在实际开发中经常会遇到给列表添加标题这样的需求,可是要给RecyclerView加个headerView或者footerView却发现没有这样的方法,怎么办呢?网上搜到的大多数方案都是通过控制Adapter的ItemType来设置的,不管是添加headerView还是footerView,它们都是Item的一种,只不过显示在特定的位置,那么我们完全可以通过为其设置ItemType来完成。所以核心就是根据不同的ItemType去加载不同的布局。
有了思路,我们就简单实现下吧
适配器代码如下:
public class HeaderAndFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private static final int ITEM_TYPE_HEADER = 1;
    private static final int ITEM_TYPE_FOOTER = 2;
    private Context mContext;
    private List<String> mDatas = new ArrayList<>();


    public HeaderAndFooterAdapter(Context mContext, List<String> mDatas) {
        this.mDatas = mDatas;
        this.mContext = mContext;
    }


    /*根据getItemViewType()方法返回的不同类型创建不同的ViewHolder*/
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == ITEM_TYPE_HEADER) {

            View view = LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent, false);
            HeaderHolder viewHolder = new HeaderHolder(view);
            return viewHolder;

        } else if (viewType == ITEM_TYPE_FOOTER) {
            View view = LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent, false);
            FootHolder viewHolder = new FootHolder(view);
            return viewHolder;
        } else {
            View view = LayoutInflater.from(mContext).inflate(R.layout.item_recyclerview_main, parent, false);
            ItemHolder viewHolder = new ItemHolder(view);
            return viewHolder;
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof HeaderHolder) {
            HeaderHolder headerHolder = (HeaderHolder) holder;
            headerHolder.textViewHeader.setText(mDatas.get(position));
        } else if (holder instanceof FootHolder) {
            FootHolder footHolder = (FootHolder) holder;
            footHolder.textViewFoot.setText(mDatas.get(position));
        } else {
            ItemHolder itemHolder = (ItemHolder) holder;
            itemHolder.textViewItem.setText(mDatas.get(position));
        }

    }

    @Override
    public int getItemCount() {
        return mDatas == null ? 0 : mDatas.size();
    }

    /*根据位置来返回不同的item类型*/
    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return ITEM_TYPE_HEADER;
        } else if (position + 1 == getItemCount()) {
            return ITEM_TYPE_FOOTER;
        } else
            return 0;
    }

    /*头部Item*/
    class HeaderHolder extends RecyclerView.ViewHolder {
        public TextView textViewHeader;

        public HeaderHolder(View itemView) {
            super(itemView);
            textViewHeader = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }
    /*底部Item*/
    class FootHolder extends RecyclerView.ViewHolder {
        public TextView textViewFoot;

        public FootHolder(View itemView) {
            super(itemView);
            textViewFoot = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }

    class ItemHolder extends RecyclerView.ViewHolder {
        public TextView textViewItem;

        public ItemHolder(View itemView) {
            super(itemView);
            textViewItem = (TextView) itemView.findViewById(R.id.tv_main_item);
        }
    }
}
  • getItemViewType
    由于我们增加了headerView和footerView首先需要复写的就是getItemCount和getItemViewType。getItemCount很好理解,对于getItemType,返回的就是item所代表的type值。
  • onCreateViewHolder
    可以看到,我们分别判断viewType,如果是headerView或者是footView,我们则为其单独创建ViewHolder。
  • onBindViewHolder
    onBindViewHolder比较简单,只要根据不同的ViewHolder来进行绑定数据即可。
Activity类如下:
public class MainActivity extends AppCompatActivity {
    private RecyclerView mRecyclerView;
    private List<String> mDatas = new ArrayList<>();
    private HeaderAndFooterAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = (RecyclerView) findViewById(R.id.ryv_main_content);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(linearLayoutManager);
        mAdapter=new HeaderAndFooterAdapter(this,mDatas);
        mRecyclerView.setAdapter(mAdapter);
        initData();
        mAdapter.notifyDataSetChanged();
    }

    private void initData() {
        mDatas.add("列表头");
        for (int i=0;i<20;i++){
            mDatas.add("item"+i);
        }
        mDatas.add("列表尾");
    }
}
效果如下:
RecyclerView添加HeaderView和FooterView
好了,现在问题来了,假设我们现在已经完成了RecyclerView的编写,忽然有个需求,需要在列表上加个HeaderView,此时我们该怎么办呢?打开我们的Adapter,然后按照我们上述的原理,添加特殊的ViewType,然后修改代码完成。这是比较常规的做法了,但是有个问题是,如果需要添加多个viewType,那么可能我们的Adapter需要修改的幅度就比较大了,比如getItemType、getItemCount、onBindViewHolder、onCreateViewHolder等,几乎所有的方法都要进行改变。如果一个项目中多个RecyclerView都需要在其列表中添加headerView,想想都头大了,所以直接改Adapter的代码是非常不划算的,最好能够设计一个类,可以无缝的为原有的Adapter添加headerView和footerView。我也是看到了鸿洋大神的这篇文章才有了收获,思路是通过类似装饰者模式,去设计一个类,增强原有Adapter的功能,使其支持addHeaderView和addFooterView。这样我们就可以不去改动我们之前已经完成的代码,灵活的去扩展功能了。幸好大学的课本没丢,又翻出出来,重新看看装饰者模式,哈哈~~

初步实现
装饰类
public class HeaderAndFooterWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private static final int BASE_ITEM_TYPE_HEADER = 0x10;
    private static final int BASE_ITEM_TYPE_FOOTER = 0x20;

    private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
    private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();

    private RecyclerView.Adapter mInnerAdapter;

    public HeaderAndFooterWrapper(RecyclerView.Adapter adapter) {
        mInnerAdapter = adapter;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (mHeaderViews.get(viewType) != null) {
            ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType));
            return holder;

        } else if (mFootViews.get(viewType) != null) {
            ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType));
            return holder;
        }
        return mInnerAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeaderViewPos(position)) {
            return mHeaderViews.keyAt(position);
        } else if (isFooterViewPos(position)) {
            return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
        }
        return mInnerAdapter.getItemViewType(position - getHeadersCount());
    }

    private int getRealItemCount() {
        return mInnerAdapter.getItemCount();
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (isHeaderViewPos(position)) {
            return;
        }
        if (isFooterViewPos(position)) {
            return;
        }
        mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
    }

    @Override
    public int getItemCount() {
        return getHeadersCount() + getFootersCount() + getRealItemCount();
    }
 
    private boolean isHeaderViewPos(int position) {
        return position < getHeadersCount();
    }

    private boolean isFooterViewPos(int position) {
        return position >= getHeadersCount() + getRealItemCount();
    }

    public void addHeaderView(View view) {
        mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
    }

    public void addFootView(View view) {
        mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
    }

    public int getHeadersCount() {
        return mHeaderViews.size();
    }

    public int getFootersCount() {
        return mFootViews.size();
    }

}
注意这几个方法:
  • getItemViewType
    可以看到我们的返回值是mHeaderViews.keyAt(position),这个值其实就是我们 addHeaderView时的key,footerView是一样的处理方式,也就是说每一个headerView都会创建不同的itemType。
  • onCreateViewHolder
    可以看到,我们分别判断viewType,如果是HeaderView或者是FooterView,我们则为其单独创建ViewHolder,这里的 ViewHolder我就直接拿鸿洋大神的用了,大家也可以自己写,只需要将对应的 headerView作为itemView传入ViewHolder的构造即可。而这里也就体现出SparseArrayCompat的作用了,对于headerView假设我们有多个,那么onCreateViewHolder返回的ViewHolder中的itemView应该对应不同的headerView,如果是List,那么不同的headerView应该对应着:list.get(0),list.get(1)等。但是onCreateViewHolder方法并没有position参数,只有itemType参数,如果itemType还是固定的一个值,那么你是没有办法根据参数得到不同的headerView的。
所以,我利用SparseArrayCompat,将其itemType作为key,value为我们的headerView,在 onCreateViewHolder中,直接通过itemType,即可获得我们的headerView,然后构造ViewHolder对象。
效果:
RecyclerView添加HeaderView和FooterView
效果差不多,我们再来看看GridLayoutMananger的效果:
RecyclerView添加HeaderView和FooterView
虽然效果不是我们想要的,但逻辑是正确的,好了,现在改下代码吧
针对GridLayoutManager
在HeaderAndFooterWrapper类中复写onAttachedToRecyclerView方法
@Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        mInnerAdapter.onAttachedToRecyclerView(recyclerView);

        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int viewType = getItemViewType(position);
                    if (mHeaderViews.get(viewType) != null) {
                        return gridLayoutManager.getSpanCount();
                    } else if (mFootViews.get(viewType) != null) {
                        return gridLayoutManager.getSpanCount();
                    }
                    if (spanSizeLookup != null)
                        return spanSizeLookup.getSpanSize(position);
                    return 1;
                }
            });
            gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
        }
    }
现在看一下运行效果:
RecyclerView添加HeaderView和FooterView
对于StaggeredGridLayoutManager
当然了,我们怎么能忘了最重要的StaggeredGridLayoutManager呢 ?StaggeredGridLayoutManager并没有setSpanSizeLookup这样的方法,但是处理依然不复杂,重写onViewAttachedToWindow方法,如下:
@Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        mInnerAdapter.onViewAttachedToWindow(holder);
        int position = holder.getLayoutPosition();
        if (isHeaderViewPos(position) || isFooterViewPos(position)) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

            if (lp != null
                    && lp instanceof StaggeredGridLayoutManager.LayoutParams) {

                StaggeredGridLayoutManager.LayoutParams p =
                        (StaggeredGridLayoutManager.LayoutParams) lp;

                p.setFullSpan(true);
            }
        }
    }
哦了,就是这样
源代码

参考:

上一篇:介绍C语言指针


下一篇:递归绑定TreeView