RecyclerView添加header与footer

529 查看

前言

这次主要关于RecyclerView添加headerfooter的实现方法,我们都知道,在使用ListView的时候我们能自由的给自己的ListView添加头部与尾部。使用addHeaderView()addFooterView()方法实现。只要自己自定义布局文件,添加进去即可。但当我们使用RecyclerView的时候就会发现这两个方便的方法没有了。那么如果此时我们要实现这些功能时要怎么办呢?不急,下面我们先来熟悉下ListView的添加headerfooter的实现原理。

ListView源码

既然ListView实现了添加头部与尾部的原理,所以我们可以先进入ListView的源码,查看其中的addHeaderView()方法与addFooterView()方法。

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;

        // Wrap the adapter if it wasn't already wrapped.
        if (mAdapter != null) {
            if (!(mAdapter instanceof HeaderViewListAdapter)) {
                mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }

            // In the case of re-adding a header view, or adding one later on,
            // we need to notify the observer.
            if (mDataSetObserver != null) {
                mDataSetObserver.onChanged();
            }
        }
    }

这里省略addFooterView()源码,其实跟addHeaderView()基本一致

通过addHeaderView()的源码我们可以发现,它是使用一个FixedViewInfo类来存储数据。

public class FixedViewInfo {
        /** The view to add to the list */
        public View view;
        /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
        public Object data;
        /** <code>true</code> if the fixed view should be selectable in the list */
        public boolean isSelectable;
    }

其中最主要的代码就是

 if (!(mAdapter instanceof HeaderViewListAdapter)) {
                mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }

我们会发现它new了一个HeaderViewListAdapter类,那么我们再进入HeaderViewListAdapter源码中,发现他就相对于一个我们自定义的Adapterheaderfooter的封装。

代码都比较简单,主要是思想,其中主要的是有

public int getCount() {
        if (mAdapter != null) {
            return getFootersCount() + getHeadersCount() + mAdapter.getCount();
        } else {
            return getFootersCount() + getHeadersCount();
        }
    }

根据position来设置ViewType的值

 public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (mAdapter != null && position >= numHeaders) {
            int adjPosition = position - numHeaders;
            int adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }

        return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    }

再看getView()方法

public View getView(int position, View convertView, ViewGroup parent) {
        // Header (negative positions will throw an IndexOutOfBoundsException)
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).view;
        }

        // Adapter
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getView(adjPosition, convertView, parent);
            }
        }

        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
        return mFooterViewInfos.get(adjPosition - adapterCount).view;
    }

getItemViewType类似,当position小于headercount时,此时返回头部View,当position减去headeercount时依然小于正常的adaptercount,此时返回正常的View,如果还有,自然剩下的就是footer了。

所以根据ListView的添加头部与尾部的实现原理,我们可以模仿实现RecyclerView的相同的功能。

EnhanceRecyclerView

那么我们根据ListView来实现一个能够添加headerfooterRecyclerView,我这里定义为EnhanceRecyclerView

FixedViewInfo

在上面我们看到ListView中使用到了FixedViewInfo,就是一个自定义的类,用来存储数据,我们做适当的修改如下,View还是不变,依然是添加的视图,将其余的删掉,换成viewType

public class FixedViewInfo {
        public View view;
        public int viewType;
    }

addHeaderView与addFooterView

做相应的修改,其中BASE_HEADER_VIEW_TYPEBASE_FOOTER_VIEW_TYPE是一个finalint数据

public void addHeaderView(View view) {
        FixedViewInfo info = new FixedViewInfo();
        info.view = view;
        info.viewType = BASE_HEADER_VIEW_TYPE + mHeaderViewInfos.size();
        mHeaderViewInfos.add(info);

        if (mAdapter != null) {
            mAdapter.notifyDataSetChanged();
        }
    }
    public void addFooterView(View view) {
        FixedViewInfo info = new FixedViewInfo();
        info.view = view;
        info.viewType = BASE_FOOTER_VIEW_TYPE + mFooterViewInfos.size();
        mFooterViewInfos.add(info);

        if (mAdapter != null) {
            mAdapter.notifyDataSetChanged();
        }
    }

setAdapter

下面是setAdapter(),使用后面封装的WrapperRecyclerViewAdapter

    @Override
    public void setAdapter(Adapter adapter) {
        if (!(adapter instanceof WrapperRecyclerViewAdapter))
            mAdapter = new WrapperRecyclerViewAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
        super.setAdapter(mAdapter);

//        if (isShouldSpan) {
//            ((WrapperRecyclerViewAdapter) mAdapter).adjustSpanSize(this);
//        }
    }

WrapperRecyclerViewAdapter

我们也可以根据ListViewHeaderViewListAdapter来模仿实现一个封装头部与尾部的adapter。我这里定义为WrapperRecyclerViewAdapter继承RecyclerView.Adapter根据情况适当修改。

getItemCount

@Override
    public int getItemCount() {
        if (mAdapter != null) {
            return getHeadersCount() + getFootersCount() + mAdapter.getItemCount();
        } else {
            return getHeadersCount() + getFootersCount();
        }
    }

getItemViewType

@Override
    public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).viewType;
        }
        int adjPosition = position - numHeaders;
        int adapterPosition = 0;
        if (mAdapter != null) {
            adapterPosition = mAdapter.getItemCount();
            if (adjPosition < adapterPosition) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }

        return mFooterViewInfos.get(position - adapterPosition - getHeadersCount()).viewType;
    }

这里逻辑与ListView的基本一致。

onCreateViewHolder

这里需将HeaderViewListAdapter中的getView()方法分成两部分来实现。

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType >= EnhanceRecyclerView.BASE_HEADER_VIEW_TYPE && viewType < EnhanceRecyclerView.BASE_HEADER_VIEW_TYPE + getHeadersCount()) {
            View view = mHeaderViewInfos.get(viewType - EnhanceRecyclerView.BASE_HEADER_VIEW_TYPE).view;
            return viewHolder(view);
        } else if (viewType >= EnhanceRecyclerView.BASE_FOOTER_VIEW_TYPE && viewType < EnhanceRecyclerView.BASE_FOOTER_VIEW_TYPE + getFootersCount()) {
            View view = mFooterViewInfos.get(viewType - EnhanceRecyclerView.BASE_FOOTER_VIEW_TYPE).view;
            return viewHolder(view);
        }
        return mAdapter.onCreateViewHolder(parent, viewType);
    }

根据不同的viewType来实现不同的布局文件.不是headerfooter布局时则直接回调原始adapteronCreateViewHolder()方法。实现正常的布局。

onBindViewHolder

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return;
        }
        int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getItemCount();
            if (adjPosition < adapterCount) {
                mAdapter.onBindViewHolder(holder, adjPosition);
                return;
            }
        }

    }

根据position的位置来选择,当position属于正常的位置范围之内时,则回调原始的adapteronBindViewHolder()方法,实现数据的绑定;对于其它的情况,无需做处理。

主要的模仿变动就是这些,然后自己在根据情况进行适当的修改,最后在布局文件中使用自定义的EnhanceRecyclerView替代RecyclerView

调整

我们都知道在使用RecyclerView的时候要设置LayoutManager,如果按照上面的实现,当LayoutManager设置为LinearLayout时,自然没上面问题,头部与尾部能正常添加;但当我们的LayoutManager设置为GridLayoutManagerStaggeredGridLayoutManager时,我们会发现头部与尾部的添加出现异常,就是他们不能独自占一行,所以这里我们要做相应的调整,使他们在添加头部与尾部的时候独自占一行。

adjustSpanSize

在上面我们自定义的WrapperRecyclerViewAdapter中添加adjustSpanSize()方法,用来实现调整。

public void adjustSpanSize(RecyclerView recyclerView) {
        if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
            final GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager();
            manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int numHeaders = getHeadersCount();
                    int adjPosition = position - numHeaders;
                    if (position < numHeaders || adjPosition >= mAdapter.getItemCount())
                        return manager.getSpanCount();
                    return 1;
                }
            });
        }

        if (recyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) {
            isStaggered = true;
        }
    }

可以看出当LayoutManagerGridLayoutManager时,我们通过manager来设置setSpanSizeLookup()getSpanSize()方法中根据具体判断来调用 manager.getSpanCount()来实现头部与尾部的占一行的效果;对于StaggeredGridLayoutManager因为其没有setSpanSizeLookup()方法,所以我们先做标记,在ViewHolder中为每一个需要的ItemView来设置paramssetFullSpa(true)方法来填充一行。

    private RecyclerView.ViewHolder viewHolder(View itemView) {

        if (isStaggered) {
            StaggeredGridLayoutManager.LayoutParams params = new StaggeredGridLayoutManager.LayoutParams(StaggeredGridLayoutManager.LayoutParams.MATCH_PARENT,
                    StaggeredGridLayoutManager.LayoutParams.WRAP_CONTENT);
            params.setFullSpan(true);
            itemView.setLayoutParams(params);
        }
        return new RecyclerView.ViewHolder(itemView) {
        };
    }

setLayoutManager

EnhanceRecyclerView中重写setLayoutManager()方法,添加标记。

@Override
    public void setLayoutManager(LayoutManager layout) {
        if (layout instanceof GridLayoutManager || layout instanceof StaggeredGridLayoutManager)
            isShouldSpan = true;
        super.setLayoutManager(layout);
    }

最后再 在EnhanceRecyclerViewsetAdapter()方法中根据标记来判断是否调用adjustSpanSize()方法来进行调整headerfooter.

if (isShouldSpan) {
            ((WrapperRecyclerViewAdapter) mAdapter).adjustSpanSize(this);
        }

其实就是前面setAdapter()中所注释的代码。

这样整个调整部分就基本完成。headerfooter的添加也就基本完成了。

总结

通过RecyclerView的头部与尾部的添加,我们应该能学习如何运用原有的资源来实现我们所需要的功能。虽然方法不一定是最好的,因为使用这种方法实现也存在一些问题,例如不能在初始化(onCreate)中同时添加headerfooter,否则会出现布局混乱的现象,现在还不知道为何有这种特例,希望知道的可以告知解决下,但这种模仿的思想还是很值得推荐的。

个人blog地址:https://idisfkj.github.io/arc...