刚接触Android开发同学可能很早就使用过ListView这个控件了,在我们做滑动列表的时候一般都会用到,然而到了Android 5.0,谷歌在新的系统特性中,提到了Matrial Design的设计风格,并且伴随着提供了一系列相关的控件,RecyclerView就是其中的控件,用过的同学都说RecyclerView可以直接替代Listview使用,是ListView的升级版,下面本人就从各个方面比较这两个控件,看看他们的区别?
知识点汇总:
一:Listview与RecyclerView实现原理的区别
二:Listview与RecyclerView接口使用的区别
三:Listview与RecyclerView优化手段及相关知识点解析
四:相关开源框架介绍与Rv控件其他UI实现
五:扩展阅读
一:Listview与RecyclerView实现原理的区别
区别一:缓存机制不同
RecyclerView比ListView多两级缓存,支持多个离开屏幕ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。
具体来说: ListView(两级缓存):
RecyclerView的四级缓存实现:
ListView和RecyclerView缓存机制对比:
1、mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;
2、mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
3、RecyclerView的优势在于a.mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;b.mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势.客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。
缓存不同:
1、RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为: View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);
2、ListView缓存View。
ListView获取缓存的流程:
RecyclerView获取缓存的流程:
区别二:缓存对象不同
1、RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:View +ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);
2、ListView缓存View。
获取缓存对比:
1、 RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView,而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)。
2、 ListView中通过pos获取的是view,即pos–>view; RecyclerView中通过pos获取的是viewholder,即pos –> (view,viewHolder,flag); 从流程图中可以看出,标志flag的作用是判断view是否需要重新bindView,这也是RecyclerView实现局部刷新的一个核心。
3、局部刷新:
由上可知,RecyclerView的缓存机制确实更加完善,但还不算质的变化,RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView,ListView和RecyclerView在数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。
区别三:数据源改变时的处理
ListView和RecyclerView在数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。
区别四:预取功能(Prefetch)
解析:Android是通过每16ms刷新一次页面来保证ui的流畅程度,现在android系统中刷新ui会通过cpu产生数据,然后交给gpu渲染的形式来完成,从上图可以看出当cpu完成数据处理交给gpu后,就一直处于空闲状态,需要等待下一帧才会进行数据处理,rv会预取接下来可能要显示的item,在下一帧到来之前提前处理完数据,然后将得到的itemholder缓存起来,等到真正要使用的时候直接从缓存取出来即可。
接口:通过LinearLayoutManager的setInitialItemPrefetchCount()。
区别五:Item回收/复用方面
解析:ListView是以convertView作为回收单位,需要手动添加ViewHolder,需要复用时常常需要设置Tag,而RecyclerView则是以ViewHolder作为回收单位,convertView被内置到了ViewHolder中作为ViewHolder的成员变量,内置了Recycle、多级缓存。
二:Listview与RecyclerView接口使用的区别
Listview:
1、使用setAdapter,重载getView()函数,getItemCount()和getItemType()函数。
RecyclerView:
1、setLayoutManager()。
2、setAdapter(),重载onBindViewHolder(),onCreateViewHolder。
3、addItemDecoration(),getItemOffset函数。
4、setItemAnimator()。
5、SnapHelper。
由于大家对ListView的使用已经比较熟悉了,这里就不描述了,后面优化部分会有Listview的代码示例,到时去一一了解,这里直接说关于RecyclerView的接口描述。
RecyclerView及相关成员介绍:
1、RecyclerView.Adapter - 处理数据集合并负责绑定视图
2、ViewHolder - 持有所有的用于绑定数据或者需要操作的View
3、LayoutManager - 负责摆放视图等相关操作
4、ItemDecoration - 负责绘制Item附近的分割线
5、ItemAnimator - 为Item的一般操作添加动画效果,如,增删条目等
图例:
Adapter类解析
1、根据不同ViewType创建与之相应的的Item-Layout。
2、访问数据集合并将数据绑定到正确的View上。
常用方法:
一般常用的重写方法有以下这么几个:
1、public VH onCreateViewHolder(ViewGroup parent, int viewType)
创建Item视图,并返回相应的ViewHolder
2、public void onBindViewHolder(VH holder, int position)
绑定数据到正确的Item视图上。
3、public int getItemCount()
返回该Adapter所持有的Itme数量
4、public int getItemViewType(int position)
用来获取当前项Item(position参数)是哪种类型的布局
ViewHolder类解析:
作用:
1、adapter应当拥有ViewHolder的子类,并且ViewHolder内部应当存储一些子view,避免时间代价很大的findViewById操作。
2、其RecyclerView内部定义的ViewHolder类包含很多复杂的属性,内部使用场景也有很多,而我们经常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一个新类型时,调用item的ViewHolder来创建一个ViewHolder,而onBindViewHolder()方法则当RecyclerView需要在特定位置的item展示数据时调用。
ViewHolder类解析:
作用:
1、adapter应当拥有ViewHolder的子类,并且ViewHolder内部应当存储一些子view,避免时间代价很大的findViewById操作。
2、其RecyclerView内部定义的ViewHolder类包含很多复杂的属性,内部使用场景也有很多,而我们经常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一个新类型时,调用item的ViewHolder来创建一个ViewHolder,而onBindViewHolder()方法则当RecyclerView需要在特定位置的item展示数据时调用。
复用机制怎样的?
1、模拟场景:只有一种ViewType,上下滑动的时候需要的ViewHolder种类是只有一种,但是需要的ViewHolder对象数量并不止一个。所以在后面创建了9个ViewHolder之后,需要的数量够了,无论怎么滑动,都只需要复用是以前创建的对象就行了。
滑动列表时,log的输出:
对于ViewHolder对象的数量“够用”之后就停止调用onCreateViewHolder方法:
1、获取为给定位置初始化的视图。
2‘此方法应由{@link LayoutManager}实现使用,以获取视图来表示来自Adapter的数据。
3、如果共享池可用于正确的视图类型,则回收程序可以重用共享池中的废视图或分离视图。如果适配器没有指示给定位置上的数据已更改,则回收程序将尝试返回一个以前为该数据初始化的报废视图,而不进行重新绑定。
LayoutManager类解析:
作用:
1、LayoutManager的职责是摆放Item的位置,并且负责决定何时回收和重用Item。
2、RecyclerView 允许自定义规则去放置子 view,这个规则的控制者就是 LayoutManager。一个 RecyclerView 如果想展示内容,就必须设置一个 LayoutManager。
LayoutManager样式有哪些?
1、LinearLayoutManager 水平或者垂直的Item视图。
2、GridLayoutManager 网格Item视图。
3、StaggeredGridLayoutManager 交错的网格Item视图。
SnapHelper类解析:
作用:
在某些场景下,卡片列表滑动浏览[有的叫轮播图],希望当滑动停止时可以将当前卡片停留在屏幕某个位置,比如停在左边,以吸引用户的焦点。那么可以使用RecyclerView + Snaphelper来实现。
LinearSnapHelper类分析:
LinearSnapHelper 使当前Item居中显示,常用场景是横向的RecyclerView,类似ViewPager效果,但是又可以快速滑动(滑动多页)。
PagerSnapHelper类分析:
PagerSnapHelper看名字可能就能猜到,使RecyclerView像ViewPager一样的效果,每次只能滑动一页(LinearSnapHelper支持快速滑动), PagerSnapHelper也是Item居中对齐。
ItemDecoration类解析:
作用:
1、通过设置recyclerView.addItemDecoration(new DividerDecoration(this));来改变Item之间的偏移量或者对Item进行装饰。
2、当然,你也可以对RecyclerView设置多个ItemDecoration,列表展示的时候会遍历所有的ItemDecoration并调用里面的绘制方法,对Item进行装饰。
自定义ItemDecoration有哪些重写方法 :
1、public void onDraw(Canvas c, RecyclerView parent)
解析:装饰的绘制在Item条目绘制之前调用,所以这有可能被Item的内容所遮挡。
2、public void onDrawOver(Canvas c, RecyclerView parent)
解析:装饰的绘制在Item条目绘制之后调用,因此装饰将浮于Item之上。
3、public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)解析:与padding或margin类似,LayoutManager在测量阶段会调用该方法,计算出每一个Item的正确尺寸并设置偏移量。
三:Listview与RecyclerView优化手段及相关知识点
Listview的优化:
1、局部刷新
2、复用ViewHolder
3、Item的布局优化(尝试使用constrantlayout)
4、getView函数,注意尽量不用做创建对象的操作
5、getView函数,注意尽量不用做复杂运算操作
6、在滑动列表时,停止图片的动态加载(需要配合图片动态请求库)
RecyclerView的优化:(ListView大部分优化点也适用与rv)
1、setHasFixSize使用(嵌套RecyclerView)
2、RecyclerViewPool的使用(嵌套RecyclerView)
3、去除默认动画效果(如果不需要)
4、设置点击事件(不算优化点)
5、diffUtils的使用
Listview的优化:
下面就展示一下, 通用getView的代码示例
@Override
public View getView(int position, View convertView, ViewGroup viewGroup) {
//不同类型的ViewHolder
TitleViewHolder titleViewHolder = null;
CompanyViewHolder contentViewHolder = null;
//对类型进行判断,分别inflate不同的布局
switch (getItemViewType(position)) {
case TYPE_TITLE:
titleViewHolder = new TitleViewHolder();
if (convertView == null) {
convertView = View.inflate(context, R.layout.view_holder_company_index, null);
titleViewHolder.title = (TextView) convertView.findViewById(R.id.tv_title);
convertView.setTag(titleViewHolder); //setTag()
} else {
titleViewHolder = (TitleViewHolder) convertView.getTag(); //getTag();
}
titleViewHolder.title.setText(mData.get(position).getName());
break;
case TYPE_CONTENT:
contentViewHolder = new CompanyViewHolder();
if (convertView == null) {
convertView = View.inflate(context, R.layout.view_holder_company, null);
contentViewHolder.content = (TextView) convertView.findViewById(R.id.tv_content);
convertView.setTag(contentViewHolder);
} else {
contentViewHolder = (CompanyViewHolder) convertView.getTag();
}
contentViewHolder.content.setText(mData.get(position).getCode());
break;
}
return convertView;
}
我们知道listview并没有提供给我们局部刷新的解接口,可能谷歌后面也意识到这个问题,在2012年的时候,官方也提供了listview局部刷新的代码示例:
google官方局部刷新代码:
private void updateSingleRow(ListView listView, long id) {
if (listView != null) {
int start = listView.getFirstVisiblePosition();
for (int i = start, j = listView.getLastVisiblePosition(); i <= j; i++)
if (id == ((Messages) listView.getItemAtPosition(i)).getId()) {
View view = listView.getChildAt(i - start);
getView(i, view, listView);
break;
}
}
}
总结:核心就是找出你要更新Item的contentView.然后再去操作。因为ListView默认只会加载一屏的数据,所以要判断其可见范围。不可见的在滑动的时候getView会自动调用更新数据,最后要强调的一点就是关于布局优化,最好将item的高度设置为一个固定的值,这样能减少getView的调用次数。因为一个不确定的值,ListView会频繁调用多次getView去确定其高度和渲染。
下面也有一些热心网友也提供了相关的局部刷新代码,大体思路其实也是一致的。
局部刷新实现:
public void updateSingleRow(ListView mListView, int posi) {
if (mListView != null) {
//获取第一个显示的item
int visiblePos = mListView.getFirstVisiblePosition();
//计算出当前选中的position和第一个的差,也就是当前在屏幕中的item位置
int offset = posi - visiblePos;
int lenth = mListView.getChildCount();
// 只有在可见区域才更新,因为不在可见区域得不到Tag,会出现空指针,所以这 是必须有的一个步骤
if ((offset < 0) || (offset >= lenth)) return;
View convertView = mListView.getChildAt(offset);
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
//以下是处理需要的控件方法。。。。。
}
}
RecyclerView的优化及相关知识点:
优化一:setHasFixedSize的使用(嵌套RecyclerView)
解析:设置固定高度的rv,避免rv重复measure调用,当item嵌套了rv,并且rv没有设置wrap_content属性时,我们可以对该rv设置setHasFixedSize,这么做的一个最大的好处就是嵌套的rv不会触发requestLayout,从而不会导致外层的rv进行重绘。
优化二:复用pool缓存(嵌套RecyclerView)
使用场景:
1、一个Rv嵌套多个Rv。
2、ViewPager中多个界面都有相同布局的Tv。
我们看到上面斗鱼App的两张图片UI,在不同的TAG,列表布局几乎完全一样,那么我们就可以尝试复用RecyclerView了。
解析:
由于ViewPager默认缓存左右两个fragment,所以进行fragment切换的时候,每次tab0和tab2都会重新生成,对应的onCreateViewHolder和onBindViewHolder都会执行。
同样的操作,因为公用了RecycledViewPool,所以切换过程中只调用了onBindViewHolder,效果很明显。
接口使用:
1、 RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
2、 recyclerView.setRecycledViewPool();
优化三:去除默认动画
解析:默认在开启item动画的情况下会使rv额外处理很多的逻辑判断,notify的增删改操作都会对应相应的item动画效果,所以如果你的应用不需要这些动画效果的话可以直接关闭掉,这样可以在处理增删改操作时大大简化rv的内部逻辑处理。
接口使用:直接调用setItemAnimator(null)即可。
优化四:设置点击事件
解析: onBindViewHolder() 中频繁创建新的 onClickListener 实例没有必要,建议实际开发中应该在 onCreateViewHolder() 中每次为新建的 View 设置一次就行。
代码实例:
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(mContext).inflate(R.layout.item_me_gv_grid, parent, false);
final MyViewHolder holder = new MyViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onItemClick(view, holder.getLayoutPosition());
}
}
});
return holder;
}
还有实现方式吗?
使用接口:addOnItemTouchListener();
优化五:diffUtils的使用
解析:diffutil是配合rv进行差异化比较的工具类,通过对比前后两个data数据集合,diffutil会自动给出一系列的notify操作,避免我们手动调用notifiy的繁琐。
使用:
1、实现DiffUtil.Callback抽象类
public abstract static class Callback {
public Callback() {
}
public abstract int getOldListSize();
public abstract int getNewListSize();
public abstract boolean areItemsTheSame(int var1, int var2);
public abstract boolean areContentsTheSame(int var1, int var2);
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return null;
}
}
2、调用代码:
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( newDiffCallBack(), true);
diffResult.dispatchUpdatesTo(adapter);
adapter.setData(newData);
最后介绍一个关于RecyclerVIew的Adapter的开源项目,BaseRecyclerViewAdapterHelper,里面就封装了DiffUtil的使用,可以查看接口setNewDiffData。
四:相关开源框架介绍与Rv实现常见UI介绍
项目一:BaseRecyclerViewAdapterHelper(18.9k)
项目简介:
1、优化Adapter代码:和原始的adapter相对,减少70%的代码量。
2、添加Item事件:Item的点击事件,Item的长按事件,Item子控件的点击事件,Item子控件的长按事件
3、添加列表加载动画:一行代码轻松切换5种默认动画。
4、添加头部、尾部:一行代码搞定,感觉又回到ListView时代。
5、自动加载:上拉加载无需监听滑动事件,可自定义加载布局,显示异常提示,自定义异常提示。同时支持下拉加载。
6、分组布局:随心定义分组头部。
7、多布局:简单配置、无需重写额外方法。
8、设置空布局:比Listview的setEmptyView还要好用。
9、添加拖拽、滑动删除:开启,监听即可,就是这么简单。
10、树形列表:比ExpandableListView还要强大,支持多级。
11、自定义ViewHolder:支持自定义ViewHolder,让开发者随心所欲。
12、扩展框架:组合第三方框架,轻松实现更多需求定制。
项目地址:https://github.com/CymChad/BaseRecyclerViewAdapterHelper
项目二:UltimateRecyclerView(7.1k)
项目简介:UltimateRecyclerview是一种功能强大的RecyclerView(advanced and flexible version ofListView),包括了下拉刷新,加载更多,多种动画,空数据提示,拖动排序,视差处理,工具栏渐变,滑动删除,自定义floating button,多种刷新效果,scrollbar等等元素。
项目地址:https://github.com/cymcsg/UltimateRecyclerView
项目三:vlayout(10k)
项目简介:VirtualLayout是一个针对RecyclerView的LayoutManager扩展,主要提供一整套布局方案和布局间的组件复用的问题。
设计思路:通过定制化的LayoutManager,接管整个RecyclerView的布局逻辑;LayoutManager管理了一系列LayoutHelper,LayoutHelper负责具体布局逻辑实现的地方;每一个LayoutHelper负责页面某一个范围内的组件布局;不同的LayoutHelper可以做不同的布局逻辑,因此可以在一个RecyclerView页面里提供异构的布局结构,这就能比系统自带的LinearLayoutManager、GridLayoutManager等提供更加丰富的能力。同时支持扩展LayoutHelper来提供更多的布局能力。
项目地址:https://github.com/alibaba/vlayout
Rv实现常见UI介绍:
一:抖音首页实现原理
旧方案思路:垂直的viewpager+下拉刷新上拉加载控件+ fragment的组合方案
新方案思路:RecyclerView+SnapHelper++ fragment的组合方案
什么是SnapHelper:
SnapHelper是一个抽象类,官方提供了一个LinearSnapHelper的子类,可以让RecyclerView滚动停止时相应的Item停留中间位置。在25.1.0版本中,官方又提供了一个PagerSnapHelper的子类,可以使RecyclerView像ViewPager一样的效果,一次只能滑一页,而且居中显示。
二:ViewPager2实现原理(继承RecyclerView)
新功能:
1、支持RTL布局(Right To Left)
2、支持竖向滚动
3、完整支持notifyDataSetChanged
API的变动:
1、FragmentStateAdapter替换了原来的FragmentStatePagerAdapter。
2、RecyclerView.Adapter替换了原来的PagerAdapter。
3、registerOnPageChangeCallback替换了原来的addPageChangeListener。
实现下滑翻页:RecyclerView+LinearLayoutManager+PagerSnapHelper
五:扩展阅读
1、http://www.sohu.com/a/300876569_611601(看完感觉我RecyclerView白学了!)
2、https://juejin.im/post/5cce410551882541e40e471d(RecyclerView问题汇总)
3、https://www.jianshu.com/p/193fb966e954(AndroidListView与RecyclerView对比浅析--缓存机制)
4、https://www.jianshu.com/p/aedb2842de30(RecyclerView性能优化|安卓offer收割基)
5、https://www.jianshu.com/p/45a43a117365(AndroidRecyclerView与ListView局部刷新)
6、https://www.jianshu.com/p/122e68e9ddac(RecycledViewPool使用)
7、https://www.jianshu.com/p/29352def27e6(RecyclerViewnotifyDataSetChanged导致图片闪烁的真凶)
8、https://blog.csdn.net/qq_21138819/article/details/83028693(【Android进阶】仿抖音系列之翻页上下滑切换视频)
9、https://juejin.im/post/5bb85f52f265da0af609c685(快速实现android版抖音主界面的心得)
10、https://www.jianshu.com/p/e0bd595d6321(【Android进阶】仿抖音系列之翻页上下滑切换视频(四))
11、https://www.jianshu.com/p/e54db232df62(让你明明白白的使用RecyclerView——SnapHelper详解)