前言
这是我之前一篇老文章了,重新整理了一下在掘金发一下,大家可以参考参考。
RecyclerView是Android 5.0版本引入的一个新的组件,目的是在一些场景中取代之前ListView和GridView,实现性能更优的解决方案。同时RecyclerView的灵活性让它可胜任更多的场景。关于RecyclerView的使用有太多的文章了,大家可以自行搜索。
我们知道RecyclerView很灵活,灵活到很多功能需要我们自己实现,比如ListView和GridView中最常用的Item点击事件。所以在使用了几次后,我准备自己封装一个WrapRecyclerView,实现一些非常常用的功能。
header&footer
在ListView中我们经常使用header和footer功能,确实也给我们带来了不少方便,而且使用场景很多。但是在RecyclerView并没有默认实现这个功能,所以WrapRecyclerView首要任务就是添加这个功能。实现后的效果图如下:
首先,我们为WrapRecyclerView创建一个内部类
WrapAdapterextendsAdapter<ViewHolder>
,同时重写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<View>(); } 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<View>(); } 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
方法加载数据。
GridLayoutManager
经过上面几步,我们已经构建了一个带有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
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设置不同的宽和高。
position
由于加入了header,item的position有了变化,导致了RecyclerView中的一些与position有关方法或使用出现问题。所以我们这里解决一些常用的方法和使用,至于其他的解决方法类似。
首先,重写几个比较常用的方法,如下:
@Override public int getChildAdapterPosition(View child) { return super.getChildAdapterPosition(child) - mWrapAdapter.getHeaderCount(); } @Override public void swapAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) { mWrapAdapter.setAdapter(adapter); super.swapAdapter(mWrapAdapter, removeAndRecycleExistingViews); } @Override public ViewHolder findViewHolderForAdapterPosition(int position) { return super.findViewHolderForAdapterPosition(position + mWrapAdapter.getHeaderCount()); } 复制代码
总之涉及到item的position的方法和使用都要格外注意,在需要的时候对position进行相应处理。
比如getChildAdapterPosition
,我们想知道item的position。但是由于RecyclerView中实际上使用的是WrapAdapter,所以获取的position是包括header的,所以要减去header的数量。
同理,在findViewHolderForAdapterPostion
中,我们想通过外部adapter中item的postion获取该item的ViewHolder。实际上我们是要在WrapAdapter中去取,这时考虑到header需要为这个position加上header的数量才能取到正确的ViewHolder。
divider
除了上面的问题,divider也会影响位置,所以也需要考虑。
在列表中我们经常会用到divider,RecyclerView并不像ListView那样可以很简单的添加divider,需要用户自定义一个ItemDecoration
。
当我们定义ItemDecoration
时就需要注意与position相关的计算,因为一般情况下divider只是给正常的item来使用,header和footer不需要使用(在其布局中已经包含了)。
由于我们一般用比较简单divider就可以了,所以这里实现了一个很简单的默认divider,如果需要自定义参考即可,代码如下:
public void setTransparentDivider(final int sizePx){ addItemDecoration(new ItemDecoration() { @Override public void onDraw(Canvas c, RecyclerView parent, State state) { super.onDraw(c, parent, state); } @Override public void onDrawOver(Canvas c, RecyclerView parent, State state) { super.onDrawOver(c, parent, state); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) { int position = ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); if(mWrapAdapter.isHeader(position) || mWrapAdapter.isFooter(position)){ super.getItemOffsets(outRect, view, parent, state); return; } int index = position - mWrapAdapter.getHeaderCount(); if(isEnd(parent, index)){ if(isVertical(parent)){ outRect.set(0, 0, 0, sizePx); } else{ outRect.set(0, 0, sizePx, 0); } } else{ outRect.set(0, 0, sizePx, sizePx); } } private boolean isVertical(RecyclerView parent){ LayoutManager layout = parent.getLayoutManager(); if(layout instanceof StaggeredGridLayoutManager){ return ((StaggeredGridLayoutManager) layout).getOrientation() == StaggeredGridLayoutManager.VERTICAL; } else if(layout instanceof LinearLayoutManager){ return ((LinearLayoutManager) layout).getOrientation() == LinearLayoutManager.VERTICAL; } return true; } private int getSpanCount(RecyclerView parent){ LayoutManager layout = parent.getLayoutManager(); if(layout instanceof GridLayoutManager){ return ((GridLayoutManager) layout).getSpanCount(); } if(layout instanceof StaggeredGridLayoutManager){ return ((StaggeredGridLayoutManager) layout).getSpanCount(); } return 1; } private boolean isEnd(RecyclerView parent, int index){ int spanCount = getSpanCount(parent); return (index + 1) % spanCount == 0; } }); } 复制代码
关于ItemDecoration的实现网上有太多的文章了,这里就不细说了。主要说说position相关需要注意的地方。
重点关注getItemOffsets
这个方法,通过getViewLayoutPosistion
获取的position是item在WrapAdapter中的position。
首先判断是否是header或footer,如果是不添加。
普通的item,由于需要判断是否是一行的最后一个isEnd
(在GridLayoutManager或StaggeredGridLayoutManager中),所以要排除掉header对item位置的影响,这里减去header的数量。
通过上面的处理,header和footer的功能基本完善了,如果遇到其他问题,可以参照上面两种情况进行处理。
下拉刷新
最后我们再为这个RecyclerView实现下拉刷新和加载更多功能。我们是基于pulltorefresh这个库。
首先要为WrapRecyclerView添加两个方法,如下:
public int getFirstVisiblePosition(){ int firstPosition = 0; LayoutManager layoutManager = getLayoutManager(); if(layoutManager instanceof LinearLayoutManager){ firstPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); } if(layoutManager instanceof GridLayoutManager){ firstPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition(); } if(layoutManager instanceof StaggeredGridLayoutManager){ int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null); firstPosition = positions[0]; } return firstPosition; } public int getLastVisiblePosition(){ int lastPosition = 0; LayoutManager layoutManager = getLayoutManager(); if(layoutManager instanceof LinearLayoutManager){ lastPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); } if(layoutManager instanceof GridLayoutManager){ lastPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); } if(layoutManager instanceof StaggeredGridLayoutManager){ int[] positions = ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(null); lastPosition = positions[positions.length - 1]; } return lastPosition; } 复制代码
这两个方法用于辅助判断滑动时是否到顶或到底,下面会用到。注意对于不同的LayoutManager使用不同的方式来获取。
新建一个PullToRefreshRecyclerView,继承PullToRefreshBase
public class PullToRefreshRecyclerView extends PullToRefreshBase<WrapRecyclerView>{ 复制代码
需要重写几个方法来实现功能,如
@Override protected boolean isReadyForPullEnd() { int lastPosition = getRefreshableView().getLastVisiblePosition(); RecyclerView.LayoutManager layoutManager = getRefreshableView().getLayoutManager(); View lastView = layoutManager.findViewByPosition(lastPosition); if(lastView != null) { int lastBottom = lastView.getBottom(); return lastPosition == getRefreshableView().getRealItemCount() - 1 && lastBottom <= getRefreshableView().getBottom(); } else{ return true; } } @Override protected boolean isReadyForPullStart() { int firstPosition = getRefreshableView().getFirstVisiblePosition(); RecyclerView.LayoutManager layoutManager = getRefreshableView().getLayoutManager(); View firstView = layoutManager.findViewByPosition(firstPosition); if(firstView != null) { int firstTop = firstView.getTop(); return firstPosition == 0 && firstTop >= 0; } else{ return true; } } 复制代码
这两个方法会在滑动的时候被调用,判断是否已经到列表顶部或底部,如果到顶部或底部就会执行下拉/上拉的操作了。
逻辑比较简单,判断是否显示了第一个/最后一个item,并且它的top/bottom也显示了(说明这个item完整显示出来了)。
还需要重写另外一个方法
@Override protected WrapRecyclerView createRefreshableView(Context context, AttributeSet attrs) { WrapRecyclerView recyclerView = new WrapRecyclerView(context, attrs); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if(isReadyForPullStart()){ recyclerView.clearFocus(); } } }); recyclerView.setId(R.id.pulltorefresh_recyclerview); return recyclerView; } 复制代码
这个方法就是创建一个WrapRecyclerView,注意不要忘了setId,否则在Fragment中使用会出现一些问题(回收重建的时候)。
由于基于pulltorefresh库,所有功能库中都实现了,所以重写这几个方法就能实现下拉刷新功能了。实现效果如下
如果想改变显示或风格,可以通过pulltorefresh库的api来实现,关于pulltorefresh库的使用大家可以自行查阅相关文档。
总结
这样关于WrapRecyclerView的功能就告一段落了,有关onClickListener
的功能很简单,大家看一下源码就明白了。