本文来自于腾讯Bugly公众号(weixinBugly),未经做者赞成,请勿转载,原文地址:http://mp.weixin.qq.com/s/CzrKotyupXbYY6EY2HP_dAhtml
RecyclerView是Android 5.0提出的新UI控件,能够用来代替传统的ListView。java
Bugly以前也发过一篇相关文章,讲解了 RecyclerView 与 ListView 在缓存机制上的一些区别:android
今天精神哥来给你们详细介绍关于 RecyclerView,你须要了解的方方面面。github
本文来自腾讯 每天P图团队——damonxia(夏正冬),Android工程师设计模式
下文中Demo的源代码地址:RecyclerViewDemo。缓存
setEmptyView()
。RecyclerView是Android 5.0提出的新UI控件,位于support-v7包中,能够经过在build.gradle中添加compile 'com.android.support:recyclerview-v7:24.2.1'
导入。微信
RecyclerView的官方定义以下:app
A flexible view for providing a limited window into a large data set.ide
从定义能够看出,flexible(可扩展性)是RecyclerView的特色。不过咱们发现和ListView有点像,本文后面会介绍RecyclerView和ListView的区别。
RecyclerView并不会彻底替代ListView(这点从ListView没有被标记为@Deprecated能够看出),二者的使用场景不同。可是RecyclerView的出现会让不少开源项目被废弃,例如横向滚动的ListView, 横向滚动的GridView, 瀑布流控件,由于RecyclerView可以实现全部这些功能。
好比有一个需求是屏幕竖着的时候的显示形式是ListView,屏幕横着的时候的显示形式是2列的GridView,此时若是用RecyclerView,则经过设置LayoutManager一行代码实现替换。
ListView相比RecyclerView,有一些优势:
addHeaderView()
, addFooterView()
添加头视图和尾视图。setOnItemClickListener()
和setOnItemLongClickListener()
设置点击事件和长按事件。这些功能在RecyclerView中都没有直接的接口,要本身实现(虽然实现起来很简单),所以若是只是实现简单的显示功能,ListView无疑更简单。
RecyclerView相比ListView,有一些明显的优势:
if(convertView == null)
的实现,并且回收机制更加完善。RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。
咱们都知道ListView经过adapter.notifyDataSetChanged()
实现ListView的更新,这种更新方法的缺点是全局更新,即对每一个Item View都进行重绘。但事实上不少时候,咱们只是更新了其中一个Item的数据,其余Item其实能够不须要重绘。
这里给出ListView实现局部更新的方法:
public void updateItemView(ListView listview, int position, Data data){ int firstPos = listview.getFirstVisiblePosition(); int lastPos = listview.getLastVisiblePosition(); if(position >= firstPos && position <= lastPos){ //可见才更新,不可见则在getView()时更新 //listview.getChildAt(i)得到的是当前可见的第i个item的view View view = listview.getChildAt(position - firstPos); VH vh = (VH)view.getTag(); vh.text.setText(data.text); } }
能够看出,咱们经过ListView的getChildAt()
来得到须要更新的View,而后经过getTag()
得到ViewHolder,从而实现更新。
RecyclerView的标准实现步骤以下:
RecyclerView.Adapter<VH>
的Adapter类(VH是ViewHolder的类名),记为NormalAdapter。RecyclerView.ViewHolder
的静态内部类,记为VH。ViewHolder的实现和ListView的ViewHolder实现几乎同样。VH onCreateViewHolder(ViewGroup parent, int viewType)
: 映射Item Layout Id,建立VH并返回。void onBindViewHolder(VH holder, int position)
: 为holder设置指定数据。int getItemCount()
: 返回Item的个数。能够看出,RecyclerView将ListView中getView()
的功能拆分红了onCreateViewHolder()
和onBindViewHolder()
。
基本的Adapter实现以下:
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.VH>{ private List<String> mDatas; public NormalAdapter(List<String> data) { this.mDatas = data; } @Override public void onBindViewHolder(VH holder, int position) { holder.title.setText(mDatas.get(position)); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //item 点击事件 } }); } @Override public int getItemCount() { return mDatas.size(); } @Override public VH onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false); return new VH(v); } public static class VH extends RecyclerView.ViewHolder{ public final TextView title; public VH(View v) { super(v); title = (TextView) v.findViewById(R.id.title); } } }
建立完Adapter,接着对RecyclerView进行设置,通常来讲,须要为RecyclerView进行四大设置,也就是后文说的四大组成:Adapter(必选),Layout Manager(必选),Item Decoration(可选,默认为空), Item Animator(可选,默认为DefaultItemAnimator)。
须要注意的是在onCreateViewHolder()
中,映射Layout必须为
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);
而不能是:
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, null);
若是要实现ListView的效果,只须要设置Adapter和Layout Manager,以下:
List<String> data = initData(); RecyclerView rv = (RecyclerView) findViewById(R.id.rv); rv.setLayoutManager(new LinearLayoutManager(this)); rv.setAdapter(new NormalAdapter(data));
ListView只提供了notifyDataSetChanged()
更新整个视图,这是很不合理的。RecyclerView提供了notifyItemInserted()
,notifyItemRemoved()
,notifyItemChanged()
等API更新单个或某个范围的Item视图。
RecyclerView的四大组成是:
Adapter的使用方式前面已经介绍了,功能就是为RecyclerView提供数据,这里主要介绍万能适配器的实现。其实万能适配器的概念在ListView就已经存在了,即base-adapter-helper。
这里咱们只针对RecyclerView,聊聊万能适配器出现的缘由。为了建立一个RecyclerView的Adapter,每次咱们都须要去作重复劳动,包括重写onCreateViewHolder()
,getItemCount()
、建立ViewHolder,而且实现过程大同小异,所以万能适配器出现了,他能经过如下方式快捷地建立一个Adapter:
mAdapter = new QuickAdapter<String>(data) { @Override public int getLayoutId(int viewType) { return R.layout.item; } @Override public void convert(VH holder, String data, int position) { holder.setText(R.id.text, data); //holder.itemView.setOnClickListener(); 此处还能够添加点击事件 } };
是否是很方便。固然复杂状况也能够轻松解决。
mAdapter = new QuickAdapter<Model>(data) { @Override public int getLayoutId(int viewType) { switch(viewType){ case TYPE_1: return R.layout.item_1; case TYPE_2: return R.layout.item_2; } } public int getItemViewType(int position) { if(position % 2 == 0){ return TYPE_1; } else{ return TYPE_2; } } @Override public void convert(VH holder, Model data, int position) { int type = getItemViewType(position); switch(type){ case TYPE_1: holder.setText(R.id.text, data.text); break; case TYPE_2: holder.setImage(R.id.image, data.image); break; } } };
这里讲解下万能适配器的实现思路。
咱们经过public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>
定义万能适配器QuickAdapter类,T是列表数据中每一个元素的类型,QuickAdapter.VH是QuickAdapter的ViewHolder实现类,称为万能ViewHolder。
首先介绍QuickAdapter.VH的实现:
static class VH extends RecyclerView.ViewHolder{ private SparseArray<View> mViews; private View mConvertView; private VH(View v){ super(v); mConvertView = v; mViews = new SparseArray<>(); } public static VH get(ViewGroup parent, int layoutId){ View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); return new VH(convertView); } public <T extends View> T getView(int id){ View v = mViews.get(id); if(v == null){ v = mConvertView.findViewById(id); mViews.put(id, v); } return (T)v; } public void setText(int id, String value){ TextView view = getView(id); view.setText(value); } }
其中的关键点在于经过SparseArray<View>
存储item view的控件,getView(int id)
的功能就是经过id得到对应的View(首先在mViews中查询是否存在,若是没有,那么findViewById()
并放入mViews中,避免下次再执行findViewById()
)。
QuickAdapter的实现以下:
public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>{ private List<T> mDatas; public QuickAdapter(List<T> datas){ this.mDatas = datas; } public abstract int getLayoutId(int viewType); @Override public VH onCreateViewHolder(ViewGroup parent, int viewType) { return VH.get(parent,getLayoutId(viewType)); } @Override public void onBindViewHolder(VH holder, int position) { convert(holder, mDatas.get(position), position); } @Override public int getItemCount() { return mDatas.size(); } public abstract void convert(VH holder, T data, int position); static class VH extends RecyclerView.ViewHolder{...} }
其中:
getLayoutId(int viewType)
是根据viewType返回布局ID。convert()
作具体的bind操做。就这样,万能适配器实现完成了。
RecyclerView经过addItemDecoration()
方法添加item之间的分割线。Android并无提供实现好的Divider,所以任何分割线样式都须要本身实现。
方法是:建立一个类并继承RecyclerView.ItemDecoration,重写如下两个方法:
Google在sample中给了一个参考的实现类:DividerItemDecoration,这里咱们经过分析这个例子来看如何自定义Item Decoration。
首先看构造函数,构造函数中得到系统属性android:listDivider
,该属性是一个Drawable对象。
所以若是要设置,则须要在value/styles.xml中设置:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="android:listDivider">@drawable/item_divider</item> </style>
接着来看getItemOffsets()
的实现:
public void getItemOffsets(Rect outRect, int position, RecyclerView parent) { if (mOrientation == VERTICAL_LIST) { outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); } }
这里只看mOrientation == VERTICAL_LIST
的状况,outRect是当前item四周的间距,相似margin属性,如今设置了该item下间距为mDivider.getIntrinsicHeight()
。
那么getItemOffsets()
是怎么被调用的呢?
RecyclerView继承了ViewGroup,并重写了measureChild()
,该方法在onMeasure()
中被调用,用来计算每一个child的大小,计算每一个child大小的时候就须要加上getItemOffsets()
设置的外间距:
public void measureChild(View child, int widthUsed, int heightUsed){ final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);//调用getItemOffsets()得到Rect对象 widthUsed += insets.left + insets.right; heightUsed += insets.top + insets.bottom; //... }
这里咱们只考虑mOrientation == VERTICAL_LIST
的状况,DividerItemDecoration的onDraw()
实际上调用了drawVertical()
:
public void drawVertical(Canvas c, RecyclerView parent) { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); /** * 画每一个item的分割线 */ for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child)); final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom);/*规定好左上角和右下角*/ mDivider.draw(c); } }
那么onDraw()
是怎么被调用的呢?还有ItemDecoration还有一个方法onDrawOver()
,该方法也能够被重写,那么onDraw()
和onDrawOver()
之间有什么关系呢?
咱们来看下面的代码:
class RecyclerView extends ViewGroup{ public void draw(Canvas c) { super.draw(c); //调用View的draw(),该方法会先调用onDraw(),再调用dispatchDraw()绘制children final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDrawOver(c, this, mState); } ... } public void onDraw(Canvas c) { super.onDraw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } } }
根据View的绘制流程,首先调用RecyclerView重写的draw()
方法,随后super.draw()
即调用View的draw()
,该方法会先调用onDraw()
(这个方法在RecyclerView重写了),再调用dispatchDraw()
绘制children。所以:ItemDecoration的onDraw()
在绘制Item以前调用,ItemDecoration的onDrawOver()
在绘制Item以后调用。
固然,若是只须要实现Item之间相隔必定距离,那么只须要为Item的布局设置margin便可,不必本身实现ItemDecoration这么麻烦。
LayoutManager负责RecyclerView的布局,其中包含了Item View的获取与回收。这里咱们简单分析LinearLayoutManager的实现。
对于LinearLayoutManager来讲,比较重要的几个方法有:
onLayoutChildren()
: 对RecyclerView进行布局的入口方法。fill()
: 负责填充RecyclerView。scrollVerticallyBy()
:根据手指的移动滑动必定距离,并调用fill()
填充。canScrollVertically()
或canScrollHorizontally()
: 判断是否支持纵向滑动或横向滑动。onLayoutChildren()
的核心实现以下:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); //将原来全部的Item View所有放到Recycler的Scrap Heap或Recycle Pool fill(recycler, mLayoutState, state, false); //填充如今全部的Item View }
RecyclerView的回收机制有个重要的概念,即将回收站分为Scrap Heap和Recycle Pool,其中Scrap Heap的元素能够被直接复用,而不须要调用onBindViewHolder()
。detachAndScrapAttachedViews()
会根据状况,将原来的Item View放入Scrap Heap或Recycle Pool,从而在复用时提高效率。
fill()
是对剩余空间不断地调用layoutChunk()
,直到填充完为止。layoutChunk()
的核心实现以下:
public void layoutChunk() { View view = layoutState.next(recycler); //调用了getViewForPosition() addView(view); //加入View measureChildWithMargins(view, 0, 0); //计算View的大小 layoutDecoratedWithMargins(view, left, top, right, bottom); //布局View }
其中next()
调用了getViewForPosition(currentPosition)
,该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View,在后文的回收机制中会介绍该方法的具体实现。
若是要自定义LayoutManager,能够参考:
RecyclerView可以经过mRecyclerView.setItemAnimator(ItemAnimator animator)
设置添加、删除、移动、改变的动画效果。RecyclerView提供了默认的ItemAnimator实现类:DefaultItemAnimator。这里咱们经过分析DefaultItemAnimator的源码来介绍如何自定义Item Animator。
DefaultItemAnimator继承自SimpleItemAnimator,SimpleItemAnimator继承自ItemAnimator。
首先咱们介绍ItemAnimator类的几个重要方法:
notifyItemChanged()
和notifyDataSetChanged()
的状况下布局发生改变时被调用。notifyItemChanged()
或notifyDataSetChanged()
时被调用。animateXxx()
返回true。上面用斜体字标识的方法比较难懂,不过不要紧,由于Android提供了SimpleItemAnimator类(继承自ItemAnimator),该类提供了一系列更易懂的API,在自定义Item Animator时只须要继承SimpleItemAnimator便可:
notifyItemChanged()
或notifyDataSetChanged()
时被调用。对于以上四个方法,注意两点:
runPendingAnimations()
中)须要调用dispatchXxxStarting(holder)
,执行完后须要调用dispatchXxxFinished(holder)
。runPendingAnimations()
中一并执行。DefaultItemAnimator类是RecyclerView提供的默认动画类。咱们经过阅读该类源码学习如何自定义Item Animator。咱们先看DefaultItemAnimator的成员变量:
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();//存放下一帧要执行的一系列add动画 ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();//存放正在执行的一批add动画 ArrayList<ViewHolder> mAddAnimations = new ArrayList<>(); //存放当前正在执行的add动画 private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>(); ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>(); private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>(); ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>(); ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>(); private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>(); ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>(); ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
DefaultItemAnimator实现了SimpleItemAnimator的animateAdd()
方法,该方法只是将该item添加到mPendingAdditions中,等到runPendingAnimations()
中执行。
public boolean animateAdd(final ViewHolder holder) { resetAnimation(holder); //重置清空全部动画 ViewCompat.setAlpha(holder.itemView, 0); //将要作动画的View先变成透明 mPendingAdditions.add(holder); return true; }
接着看runPendingAnimations()
的实现,该方法是执行remove,move,change,add动画,执行顺序为:remove动画最早执行,随后move和change并行执行,最后是add动画。为了简化,咱们将remove,move,change动画执行过程省略,只看执行add动画的过程,以下:
public void runPendingAnimations() { //一、判断是否有动画要执行,即各个动画的成员变量里是否有值。 //二、执行remove动画 //三、执行move动画 //四、执行change动画,与move动画并行执行 //五、执行add动画 if (additionsPending) { final ArrayList<ViewHolder> additions = new ArrayList<>(); additions.addAll(mPendingAdditions); mAdditionsList.add(additions); mPendingAdditions.clear(); Runnable adder = new Runnable() { @Override public void run() { for (ViewHolder holder : additions) { animateAddImpl(holder); //***** 执行动画的方法 ***** } additions.clear(); mAdditionsList.remove(additions); } }; if (removalsPending || movesPending || changesPending) { long removeDuration = removalsPending ? getRemoveDuration() : 0; long moveDuration = movesPending ? getMoveDuration() : 0; long changeDuration = changesPending ? getChangeDuration() : 0; long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); View view = additions.get(0).itemView; ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); //等remove,move,change动画所有作完后,开始执行add动画 } } }
为了防止在执行add动画时外面有新的add动画添加到mPendingAdditions中,从而致使执行add动画错乱,这里将mPendingAdditions的内容移动到局部变量additions中,而后遍历additions执行动画。
在runPendingAnimations()
中,animateAddImpl()
是执行add动画的具体方法,其实就是将itemView的透明度从0变到1(在animateAdd()
中已经将view的透明度变为0),实现以下:
void animateAddImpl(final ViewHolder holder) { final View view = holder.itemView; final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); mAddAnimations.add(holder); animation.alpha(1).setDuration(getAddDuration()). setListener(new VpaListenerAdapter() { @Override public void onAnimationStart(View view) { dispatchAddStarting(holder); //在开始add动画前调用 } @Override public void onAnimationCancel(View view) { ViewCompat.setAlpha(view, 1); } @Override public void onAnimationEnd(View view) { animation.setListener(null); dispatchAddFinished(holder); //在结束add动画后调用 mAddAnimations.remove(holder); if (!isRunning()) { dispatchAnimationsFinished(); //结束全部动画后调用 } } }).start(); }
从DefaultItemAnimator类的实现来看,发现自定义Item Animator好麻烦,须要继承SimpleItemAnimator类,而后实现一堆方法。别急,recyclerview-animators解救你,缘由以下:
首先,recyclerview-animators提供了一系列的Animator,好比FadeInAnimator,ScaleInAnimator。其次,若是该库中没有你满意的动画,该库提供了BaseItemAnimator类,该类继承自SimpleItemAnimator,进一步封装了自定义Item Animator的代码,使得自定义Item Animator更方便,你只须要关注动画自己。若是要实现DefaultItemAnimator的代码,只须要如下实现:
public class DefaultItemAnimator extends BaseItemAnimator { public DefaultItemAnimator() { } public DefaultItemAnimator(Interpolator interpolator) { mInterpolator = interpolator; } @Override protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) { ViewCompat.animate(holder.itemView) .alpha(0) .setDuration(getRemoveDuration()) .setListener(new DefaultRemoveVpaListener(holder)) .setStartDelay(getRemoveDelay(holder)) .start(); } @Override protected void preAnimateAddImpl(RecyclerView.ViewHolder holder) { ViewCompat.setAlpha(holder.itemView, 0); //透明度先变为0 } @Override protected void animateAddImpl(final RecyclerView.ViewHolder holder) { ViewCompat.animate(holder.itemView) .alpha(1) .setDuration(getAddDuration()) .setListener(new DefaultAddVpaListener(holder)) .setStartDelay(getAddDelay(holder)) .start(); } }
是否是比继承SimpleItemAnimator方便多了。
对于RecyclerView的Item Animator,有一个常见的坑就是"闪屏问题"。这个问题的描述是:当Item视图中有图片和文字,当更新文字并调用notifyItemChanged()
时,文字改变的同时图片会闪一下。这个问题的缘由是当调用notifyItemChanged()
时,会调用DefaultItemAnimator的animateChangeImpl()
执行change动画,该动画会使得Item的透明度从0变为1,从而形成闪屏。
解决办法很简单,在rv.setAdapter()
以前调用((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)
禁用change动画。
RecyclerView默认没有像ListView同样提供setOnItemClickListener()
接口,而RecyclerView没法添加onItemClickListener最佳的高效解决方案这篇文章给出了经过recyclerView.addOnItemTouchListener(...)
添加点击事件的方法,但我认为根本没有必要费这么大劲对外暴露这个接口,由于咱们彻底能够把点击事件的实现写在Adapter的onBindViewHolder()
中,不暴露出来。具体方法就是经过:
public void onBindViewHolder(VH holder, int position) { holder.itemView.setOnClickListener(...); }
RecyclerView默认没有提供相似addHeaderView()
和addFooterView()
的API,所以这里介绍如何优雅地实现这两个接口。
若是你已经实现了一个Adapter,如今想为这个Adapter添加addHeaderView()
和addFooterView()
接口,则须要在Adapter中添加几个Item Type,而后修改getItemViewType()
,onCreateViewHolder()
,onBindViewHolder()
,getItemCount()
等方法,并添加switch语句进行判断。那么如何在不破坏原有Adapter实现的状况下完成呢?
这里引入装饰器(Decorator)设计模式,该设计模式经过组合的方式,在不破话原有类代码的状况下,对原有类的功能进行扩展。
这偏偏知足了咱们的需求。咱们只须要经过如下方式为原有的Adapter(这里命名为NormalAdapter)添加addHeaderView()
和addFooterView()
接口:
NormalAdapter adapter = new NormalAdapter(data); NormalAdapterWrapper newAdapter = new NormalAdapterWrapper(adapter); View headerView = LayoutInflater.from(this).inflate(R.layout.item_header, mRecyclerView, false); View footerView = LayoutInflater.from(this).inflate(R.layout.item_footer, mRecyclerView, false); newAdapter.addFooterView(footerView); newAdapter.addHeaderView(headerView); mRecyclerView.setAdapter(newAdapter);
是否是看起来特别优雅。具体实现思路其实很简单,建立一个继承RecyclerView.Adapter<RecyclerView.ViewHolder>
的类,并重写常见的方法,而后经过引入ITEM TYPE的方式实现:
public class NormalAdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder>{ enum ITEM_TYPE{ HEADER, FOOTER, NORMAL } private NormalAdapter mAdapter; private View mHeaderView; private View mFooterView; public NormalAdapterWrapper(NormalAdapter adapter){ mAdapter = adapter; } @Override public int getItemViewType(int position) { if(position == 0){ return ITEM_TYPE.HEADER.ordinal(); } else if(position == mAdapter.getItemCount() + 1){ return ITEM_TYPE.FOOTER.ordinal(); } else{ return ITEM_TYPE.NORMAL.ordinal(); } } @Override public int getItemCount() { return mAdapter.getItemCount() + 2; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(position == 0){ return; } else if(position == mAdapter.getItemCount() + 1){ return; } else{ mAdapter.onBindViewHolder(((NormalAdapter.VH)holder), position - 1); } } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if(viewType == ITEM_TYPE.HEADER.ordinal()){ return new RecyclerView.ViewHolder(mHeaderView) {}; } else if(viewType == ITEM_TYPE.FOOTER.ordinal()){ return new RecyclerView.ViewHolder(mFooterView) {}; } else{ return mAdapter.onCreateViewHolder(parent,viewType); } } public void addHeaderView(View view){ this.mHeaderView = view; } public void addFooterView(View view){ this.mFooterView = view; } }
ListView提供了setEmptyView()
设置Adapter数据为空时的View视图。RecyclerView虽然没提供直接的API,可是也能够很简单地实现。
getRootView().addView(emptyView)
将空数据时显示的View添加到当前View的层次结构中。具体实现以下:
public class EmptyRecyclerView extends RecyclerView{ private View mEmptyView; private AdapterDataObserver mObserver = new AdapterDataObserver() { @Override public void onChanged() { Adapter adapter = getAdapter(); if(adapter.getItemCount() == 0){ mEmptyView.setVisibility(VISIBLE); EmptyRecyclerView.this.setVisibility(GONE); } else{ mEmptyView.setVisibility(GONE); EmptyRecyclerView.this.setVisibility(VISIBLE); } } public void onItemRangeChanged(int positionStart, int itemCount) {onChanged();} public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {onChanged();} public void onItemRangeRemoved(int positionStart, int itemCount) {onChanged();} public void onItemRangeInserted(int positionStart, int itemCount) {onChanged();} public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {onChanged();} }; public EmptyRecyclerView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public void setEmptyView(View view){ this.mEmptyView = view; ((ViewGroup)this.getRootView()).addView(mEmptyView); //加入主界面布局 } public void setAdapter(RecyclerView.Adapter adapter){ super.setAdapter(adapter); adapter.registerAdapterDataObserver(mObserver); mObserver.onChanged(); } }
Android提供了ItemTouchHelper类,使得RecyclerView可以轻易地实现滑动和拖拽,此处咱们要实现上下拖拽和侧滑删除。首先建立一个继承自ItemTouchHelper.Callback
的类,并重写如下方法:
getMovementFlags()
: 设置支持的拖拽和滑动的方向,此处咱们支持的拖拽方向为上下,滑动方向为从左到右和从右到左,内部经过makeMovementFlags()
设置。onMove()
: 拖拽时回调。onSwiped()
: 滑动时回调。onSelectedChanged()
: 状态变化时回调,一共有三个状态,分别是ACTION_STATE_IDLE(空闲状态),ACTION_STATE_SWIPE(滑动状态),ACTION_STATE_DRAG(拖拽状态)。此方法中能够作一些状态变化时的处理,好比拖拽的时候修改背景色。clearView()
: 用户交互结束时回调。此方法能够作一些状态的清空,好比拖拽结束后还原背景色。isLongPressDragEnabled()
: 是否支持长按拖拽,默认为true。若是不想支持长按拖拽,则重写并返回false。具体实现以下:
public class SimpleItemTouchCallback extends ItemTouchHelper.Callback { private NormalAdapter mAdapter; private List<ObjectModel> mData; public SimpleItemTouchCallback(NormalAdapter adapter, List<ObjectModel> data){ mAdapter = adapter; mData = data; } @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; //s上下拖拽 int swipeFlag = ItemTouchHelper.START | ItemTouchHelper.END; //左->右和右->左滑动 return makeMovementFlags(dragFlag,swipeFlag); } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int from = viewHolder.getAdapterPosition(); int to = target.getAdapterPosition(); Collections.swap(mData, from, to); mAdapter.notifyItemMoved(from, to); return true; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { int pos = viewHolder.getAdapterPosition(); mData.remove(pos); mAdapter.notifyItemRemoved(pos); } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { super.onSelectedChanged(viewHolder, actionState); if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){ NormalAdapter.VH holder = (NormalAdapter.VH)viewHolder; holder.itemView.setBackgroundColor(0xffbcbcbc); //设置拖拽和侧滑时的背景色 } } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); NormalAdapter.VH holder = (NormalAdapter.VH)viewHolder; holder.itemView.setBackgroundColor(0xffeeeeee); //背景色还原 } }
而后经过如下代码为RecyclerView设置该滑动、拖拽功能:
ItemTouchHelper helper = new ItemTouchHelper(new SimpleItemTouchCallback(adapter, data)); helper.attachToRecyclerView(recyclerview);
前面拖拽的触发方式只有长按,若是想支持触摸Item中的某个View实现拖拽,则核心方法为helper.startDrag(holder)
。首先定义接口:
interface OnStartDragListener{ void startDrag(RecyclerView.ViewHolder holder); }
而后让Activity实现该接口:
public MainActivity extends Activity implements OnStartDragListener{ ... public void startDrag(RecyclerView.ViewHolder holder) { mHelper.startDrag(holder); } }
若是要对ViewHolder的text对象支持触摸拖拽,则在Adapter中的onBindViewHolder()
中添加:
holder.text.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN){ mListener.startDrag(holder); } return false; } });
其中mListener是在建立Adapter时将实现OnStartDragListener接口的Activity对象做为参数传进来。
ListView为了保证Item View的复用,实现了一套回收机制,该回收机制的实现类是RecycleBin,他实现了两级缓存:
View[] mActiveViews
: 缓存屏幕上的View,在该缓存里的View不须要调用getView()
。ArrayList<View>[] mScrapViews;
: 每一个Item Type对应一个列表做为回收站,缓存因为滚动而消失的View,此处的View若是被复用,会以参数的形式传给getView()
。接下来咱们经过源码分析ListView是如何与RecycleBin交互的。其实ListView和RecyclerView的layout过程大同小异,ListView的布局函数是layoutChildren()
,实现以下:
void layoutChildren(){ //1. 若是数据被改变了,则将全部Item View回收至scrapView //(而RecyclerView会根据状况放入Scrap Heap或RecyclePool);不然回收至mActiveViews if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } //2. 填充 switch(){ case LAYOUT_XXX: fillXxx(); break; case LAYOUT_XXX: fillXxx(); break; } //3. 回收多余的activeView mRecycler.scrapActiveViews(); }
其中fillXxx()
实现了对Item View进行填充,该方法内部调用了makeAndAddView()
,实现以下:
View makeAndAddView(){ if (!mDataChanged) { child = mRecycler.getActiveView(position); if (child != null) { return child; } } child = obtainView(position, mIsScrap); return child; }
其中,getActiveView()
是从mActiveViews中获取合适的View,若是获取到了,则直接返回,而不调用obtainView()
,这也印证了若是从mActiveViews获取到了可复用的View,则不须要调用getView()
。
obtainView()
是从mScrapViews中获取合适的View,而后以参数形式传给了getView()
,实现以下:
View obtainView(int position){ final View scrapView = mRecycler.getScrapView(position); //从RecycleBin中获取复用的View final View child = mAdapter.getView(position, scrapView, this); }
接下去咱们介绍getScrapView(position)
的实现,该方法经过position获得Item Type,而后根据Item Type从mScrapViews获取可复用的View,若是获取不到,则返回null,具体实现以下:
class RecycleBin{ private View[] mActiveViews; //存储屏幕上的View private ArrayList<View>[] mScrapViews; //每一个item type对应一个ArrayList private int mViewTypeCount; //item type的个数 private ArrayList<View> mCurrentScrap; //mScrapViews[0] View getScrapView(int position) { final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap < 0) { return null; } if (mViewTypeCount == 1) { return retrieveFromScrap(mCurrentScrap, position); } else if (whichScrap < mScrapViews.length) { return retrieveFromScrap(mScrapViews[whichScrap], position); } return null; } private View retrieveFromScrap(ArrayList<View> scrapViews, int position){ int size = scrapViews.size(); if(size > 0){ return scrapView.remove(scrapViews.size() - 1); //从回收列表中取出最后一个元素复用 } else{ return null; } } }
RecyclerView和ListView的回收机制很是类似,可是ListView是以View做为单位进行回收,RecyclerView是以ViewHolder做为单位进行回收。Recycler是RecyclerView回收机制的实现类,他实现了四级缓存:
getView()
。在上文Layout Manager中已经介绍了RecyclerView的layout过程,可是一笔带过了getViewForPosition()
,所以此处介绍该方法的实现。
View getViewForPosition(int position, boolean dryRun){ if(holder == null){ //从mAttachedScrap,mCachedViews获取ViewHolder holder = getScrapViewForPosition(position,INVALID,dryRun); //此处得到的View不须要bind } final int type = mAdapter.getItemViewType(offsetPosition); if (mAdapter.hasStableIds()) { //默认为false holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); } if(holder == null && mViewCacheExtension != null){ final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type); //从 if(view != null){ holder = getChildViewHolder(view); } } if(holder == null){ holder = getRecycledViewPool().getRecycledView(type); } if(holder == null){ //没有缓存,则建立 holder = mAdapter.createViewHolder(RecyclerView.this, type); //调用onCreateViewHolder() } if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){ mAdapter.bindViewHolder(holder, offsetPosition); } return holder.itemView; }
从上述实现能够看出,依次从mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool寻找可复用的ViewHolder,若是是从mAttachedScrap或mCachedViews中获取的ViewHolder,则不会调用onBindViewHolder()
,mAttachedScrap和mCachedViews也就是咱们所说的Scrap Heap;而若是从mViewCacheExtension或mRecyclerPool中获取的ViewHolder,则会调用onBindViewHolder()
。
RecyclerView局部刷新的实现原理也是基于RecyclerView的回收机制,即能直接复用的ViewHolder就不调用onBindViewHolder()
。
Android 5.0推出了嵌套滑动机制,在以前,一旦子View处理了触摸事件,父View就没有机会再处理此次的触摸事件,而嵌套滑动机制解决了这个问题,可以实现以下效果:
为了支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口,而RecyclerView实现了NestedScrollingChild接口,而CoordinatorLayout实现了NestedScrollingParent接口,上图是实现CoordinatorLayout嵌套RecyclerView的效果。
为了实现上图的效果,须要用到的组件有:
实现中须要注意的点有:
app:layout_collapseMode
设置为pin,表示折叠以后固定在顶端,而为ImageView的app:layout_collapseMode
设置为parallax,表示视差模式,即渐变的效果。app:layout_behavior="@string/appbar_scrolling_view_behavior"
。app:layout_scrollFlags="scroll|exitUntilCollapsed"
,其中scroll表示滚动出屏幕,exitUntilCollapsed表示退出后折叠。具体实现参见Demo6。
回顾整篇文章,发现咱们已经实现了RecyclerView的不少扩展功能,包括:打造万能适配器、添加Item事件、添加头视图和尾视图、设置空布局、侧滑拖拽。BaseRecyclerViewAdapterHelper是一个比较火的RecyclerView扩展库,仔细一看发现,这里面80%的功能在咱们这篇文章中都实现了。
更多精彩内容欢迎关注腾讯 Bugly的微信公众帐号:
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!