介绍
从Android 5.0开始,谷歌公司推出了一个用于大量数据展示的新控件RecylerView,可以用来代替传统的ListView,更加强大和灵活。
RecyclerView的官方定义 A flexible view for providing a limited window into a large data set. 一种灵活的视图,用于为大型数据集提供有限的窗口。
从定义可以看出,flexible(可扩展性)是RecyclerView的特点。
RecyclerView是support-v7包中的组件现在已经移动到了androidx.recyclerview.widget,是一个强大的滑动组件,与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字Recyclerview即回收view也可以看出。
用处
这是一个强大的滑动组件,用于在视图展示一个可滑动的列表。支持多种样式的列表 -> 横竖、网格和瀑布流。
优点
- RecyclerView封装了Viewholder的回收复用,也就是说RecyclerView标准化了Viewholder,编写Adapter面向的是Viewholder而不再是View了,复用的逻辑被封装了,写起来更加简单。
- 提供了一种插拔式的体验,高度的解耦,异常的灵活,针对一个Item的显示RecyclerView专门抽取出了相应的类,来控制Item的显示,使其的扩展性非常强。
- 设置布局管理器以控制Item的布局方式,横向、竖向以及瀑布流方式,也就是说RecyclerView不再拘泥于ListView的线性展示方式,它也可以实现GridView的效果等多种效果。
- 可设置Item的间隔样式(可绘制),通过继承RecyclerView的ItemDecoration这个类,然后针对自己的业务需求去书写代码。
- 可以控制Item增删的动画,可以通过ItemAnimator这个类进行控制,当然针对增删的动画,RecyclerView有其自己默认的实现。
原理
RecyclerView的基本使用
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
//设置布局管理器
recyclerView.setLayoutManager(layoutManager);
//设置为垂直布局,这也是默认的
layoutManager.setOrientation(OrientationHelper. VERTICAL);
//设置Adapter
recyclerView.setAdapter(recycleAdapter);
//设置分隔线
recyclerView.addItemDecoration(new DividerGridItemDecoration(this));
//设置增加或删除条目的动画
recyclerView.setItemAnimator(new DefaultItemAnimator());
在使用RecyclerView时候,必须指定一个适配器Adapter和一个布局管理器LayoutManager。适配器继承RecyclerView.Adapter类,具体实现类似ListView的适配器,取决于数据信息以及展示的UI。布局管理器用于确定RecyclerView中Item的展示方式以及决定何时复用已经不可见的Item,避免重复创建以及执行高成本的findViewById()方法。
可以看见RecyclerView相比ListView会多出许多操作,这也是RecyclerView灵活的地方,它将许多动能暴露出来,用户可以选择性的自定义属性以满足需求。
四大组成
- Layout Manager:Item的布局。
- Adapter:为Item提供数据。
- Item Decoration:Item之间的Divider。
- Item Animator:添加、删除Item动画。
Layout Manager布局管理器
RecyclerView能够支持各种各样的布局效果,这是 ListView所不具有的功能,那么这个功能如何实现的呢?其核心关键在于 RecyclerView.LayoutManager 类中。从前面的基础使用可以看到,RecyclerView在使用过程中要比 ListView多一个 setLayoutManager 步骤,这个 LayoutManager就是用于控制我们 RecyclerView最终的展示效果的。
LayoutManager负责 RecyclerView的布局,其中包含了Item View的获取与回收。
RecyclerView提供了三种布局管理器:
- LinerLayoutManager 以垂直或者水平列表方式展示Item
- GridLayoutManager 以网格方式展示Item
- StaggeredGridLayoutManager 以瀑布流方式展示Item
如果你想用 RecyclerView来实现自己自定义效果,则应该去继承实现自己的 LayoutManager,并重写相应的方法,而不应该想着去改写 RecyclerView。
对于LinearLayoutManager来说,比较重要的几个方法有:
onLayoutChildren(): 对RecyclerView进行布局的入口方法
fill(): 负责填充RecyclerView
scrollVerticallyBy():根据手指的移动滑动一定距离,并调用fill()填充
canScrollVertically()或canScrollHorizontally(): 判断是否支持纵向滑动或横向滑动
fill()是对剩余空间不断地调用layoutChunk(),直到填充完为止。
//SDK 28
public void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);//调用了getViewForPosition()
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
//添加View
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
...,
measureChildWithMargins(view, 0, 0); //计算View的大小
...,
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);//布局View
...,
}
其中next()调用了getViewForPosition(currentPosition),该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View,在后文的回收机制中会介绍该方法的具体实现。
Adapter适配器和ViewHolder
Adapter的功能就是为RecyclerView提供数据,为了创建一个RecyclerView的Adapter,和ListView的Adapter类似,都是用来展示和绑定ItemView的。
主要方法
- onCreateViewHolder(@NonNull ViewGroup parent, int viewType):创建ViewHolder
- onBindViewHolder(@NonNull VH holder, int position):绑定ViewHolder
- getItemCount():列表数量
Adapter主要是操作ViewHolder,并不是直接操作View视图,根据方法去创建和绑定ViewHolder,让ViewHodler去根据数据操作视图。
Item Decoration
RecyclerView通过addItemDecoration()方法添加item之间的分割线。Android并没有提供实现好的Divider,因此任何分割线样式都需要自己实现。
自定义间隔样式需要继承RecyclerView.ItemDecoration类,该类是个抽象类,官方目前并没有提供默认的实现类,主要有三个方法。
- onDraw(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式。
- onDrawOver(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式。
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state),设置item的偏移量,偏移的部分用于填充间隔样式,即设置分割线的宽、高;在RecyclerView的onMesure()中会调用该方法。
- onDraw()和onDrawOver()这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。
Item Animator
RecyclerView能够通过RecyclerView.setItemAnimator(ItemAnimator animator)设置添加、删除、移动、改变的动画效果。
RecyclerView提供了默认的ItemAnimator实现类:DefaultItemAnimator。如果没有特殊的需求,默认使用这个动画即可。
DefaultItemAnimator继承自SimpleItemAnimator,SimpleItemAnimator继承自ItemAnimator。
首先我们介绍ItemAnimator类的几个重要方法:
- animateAppearance(): 当ViewHolder出现在屏幕上时被调用(可能是add或move)。
- animateDisappearance(): 当ViewHolder消失在屏幕上时被调用(可能是remove或move)。
- animatePersistence(): 在没调用notifyItemChanged()和notifyDataSetChanged()的情况下布局发生改变时被调用。
- animateChange(): 在显式调用notifyItemChanged()或notifyDataSetChanged()时被调用。
- runPendingAnimations(): RecyclerView动画的执行方式并不是立即执行,而是每帧执行一次,比如两帧之间添加了多个Item,则会将这些将要执行的动画Pending住,保存在成员变量中,等到下一帧一起执行。该方法执行的前提是前面animateXxx()返回true。
- isRunning(): 是否有动画要执行或正在执行。
- dispatchAnimationsFinished(): 当全部动画执行完毕时被调用。
SimpleItemAnimator类(继承自ItemAnimator),该类提供了一系列更易懂的API,在自定义Item Animator时只需要继承SimpleItemAnimator即可:
- animateAdd(ViewHolder holder): 当Item添加时被调用。
- animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY): 当Item移动时被调用。
- animateRemove(ViewHolder holder): 当Item删除时被调用。
- animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop): 当显式调用notifyItemChanged()或notifyDataSetChanged()时被调用。
以上四个方法,注意两点:
当Xxx动画开始执行前(在runPendingAnimations()中)需要调用dispatchXxxStarting(holder),执行完后需要调用dispatchXxxFinished(holder)。
这些方法的内部实际上并不是书写执行动画的代码,而是将需要执行动画的Item全部存入成员变量中,并且返回值为true,然后在runPendingAnimations()中一并执行。
嵌套滑动机制
Android 5.0推出了嵌套滑动机制,在之前,一旦子View处理了触摸事件,父View就没有机会再处理这次的触摸事件,而嵌套滑动机制解决了这个问题。
为了支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口。
提问
1.RecyclerView并没有像ListView一样暴露出Item点击事件或者长按事件处理的api,也就是说使用RecyclerView时候,需要我们自己来实现Item的点击和长按等事件的处理
实现方法有很多:
- 可以监听RecyclerView的Touch事件然后判断手势做相应的处理,
- 也可以通过在绑定ViewHolder的时候设置监听,然后通过Apater回调出去
2.局部刷新闪屏问题
对于RecyclerView的Item Animator,有一个常见的坑就是“闪屏问题”。
这个问题的描述是:当Item视图中有图片和文字,当更新文字并调用notifyItemChanged()时,文字改变的同时图片会闪一下。这个问题的原因是当调用notifyItemChanged()时,会调用DefaultItemAnimator的animateChangeImpl()执行change动画,该动画会使得Item的透明度从0变为1,从而造成闪屏。
解决办法很简单,在rv.setAdapter()之前调用((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)禁用change动画。
RecyclerView vs ListView
ListView相比RecyclerView,有一些优点:
- addHeaderView(), addFooterView()添加头视图和尾视图。
- 通过android:divider设置自定义分割线。
- setOnItemClickListener()和setOnItemLongClickListener()设置点击事件和长按事件。这些功能在RecyclerView中都没有直接的接口,要自己实现(虽然实现起来很简单),因此如果只是实现简单的显示功能,ListView无疑更简单。
RecyclerView相比ListView,有一些明显的优点:
- 默认已经实现了View的复用,不需要类似if(convertView == null)的实现,而且回收机制更加完善。
- 默认支持局部刷新。
- 容易实现添加item、删除item的动画效果。
- 容易实现拖拽、侧滑删除等功能。
- 配置不同的LayoutManaer可以实现不同的布局效果,而ListView只能实现垂直列表。
RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。
局部刷新
ListView实现局部刷新
我们都知道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提供了notifyItemInserted(),notifyItemRemoved(),notifyItemChanged()等API更新单个或某个范围的Item视图。
缓存机制对比
ListView与RecyclerView缓存机制原理大致相似
在列表滑动的过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异。
1.层级不同
RecyclerView比ListView多两级缓存,支持多个离屏ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。RecyclerView的缓存在Recycler类中。
ListView
ListView缓存.png
RecyclerView
RecyclerView缓存.png
ListView和RecyclerView缓存机制基本一致:
- mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;
- mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
- RecyclerView的优势在于
1).mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;
2).mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势。
客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。
2.缓存不同
- RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:
View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态); - ListView缓存View。
缓存不同,二者在缓存的使用上也略有差别,具体来说:
ListView获取缓存的流程:
ListView读取缓存.png
RecyclerView获取缓存的流程:
RecyclerView读取缓存.png
- RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView。
而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)。
//AbsListView源码:line2365 SDK28
View obtainView(int position, boolean[] outMetadata) {
//通过匹配pos从mScrapView中获取缓存
final View scrapView = mRecycler.getScrapView(position);
//无论是否成功都直接调用getView,导致必定会调用createView
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else {
...
}
}
}
//CursorAdapter为例
public View getView(int position, View convertView, ViewGroup parent) {
...
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
- ListView中通过pos获取的是view,即pos–>view;
RecyclerView中通过pos获取的是viewholder,即pos –> (view,viewHolder,flag);
从流程图中可以看出,标志flag的作用是判断view是否需要重新bindView,这也是RecyclerView实现局部刷新的一个核心。
局部刷新
由上文可知,RecyclerView的缓存机制确实更加完善,但还不算质的变化,RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView。
结合RecyclerView的缓存机制,看看局部刷新是如何实现的:
以RecyclerView中notifyItemRemoved(1)为例,最终会调用requestLayout(),使整个RecyclerView重新绘制,过程为:
onMeasure()–>onLayout()–>onDraw()
其中,onLayout()为重点,分为三步:
- dispathLayoutStep1():记录RecyclerView刷新前列表项ItemView的各种信息,如Top,Left,Bottom,Right,用于动画的相关计算;
- dispathLayoutStep2():真正测量布局大小,位置,核心函数为layoutChildren();
-
dispathLayoutStep3():计算布局前后各个ItemView的状态,如Remove,Add,Move,Update等,如有必要执行相应的动画.
layoutChildren流程图.png
当调用notifyItemRemoved时,会对屏幕内ItemView做预处理,修改ItemView相应的pos以及flag(流程图中红色部分):
1.png
当调用fill()中RecyclerView.getViewForPosition(pos)时,RecyclerView通过对pos和flag的预处理,使得bindview只调用一次.
需要指出,ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。
回收机制源码分析
ListView回收机制
ListView为了保证Item View的复用,实现了一套回收机制,该回收机制的实现类是RecycleBin,他实现了两级缓存:
- View[] mActiveViews: 缓存屏幕上的View,在该缓存里的View不需要调用getView()。
- ArrayList<View>[] mScrapViews;: 每个Item Type对应一个列表作为回收站,缓存由于滚动而消失的View,此处的View如果被复用,会以参数的形式传给getView()。
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回收机制
RecyclerView和ListView的回收机制非常相似,但是ListView是以View作为单位进行回收,RecyclerView是以ViewHolder作为单位进行回收。
Recycler是RecyclerView回收机制的实现类,他实现了四级缓存:
- mAttachedScrap: 缓存在屏幕上的ViewHolder。
- mCachedViews: 缓存屏幕外的ViewHolder,默认为2个。ListView对于屏幕外的缓存都会调用getView()。
- mViewCacheExtensions: 需要用户定制,默认不实现。
- mRecyclerPool: 缓存池,多个RecyclerView共用。
//参考SDK28
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
//第一个指向的是mChangedScrap缓存列表,如果有一个变化的ViewHolder,那么会从这个列表中获取,会执行bindViewHodler方法
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
//从mAttachedScrap,mCachedViews获取ViewHolder 1),2)
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
if (holder == null) {
// 2) Find from scrap/cache via stable ids, if exists
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
}
if (holder == null && mViewCacheExtension != null) {
//从开发者自定义的缓存中获取
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
}
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;
}
依次从mChangedScrap,mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool寻找可复用的ViewHolder,如果是从mAttachedScrap或mCachedViews中获取的ViewHolder,则不会调用onBindViewHolder(),mAttachedScrap和mCachedViews也就是我们所说的Scrap Heap;而如果从mChangedScrap、mViewCacheExtension或mRecyclerPool中获取的ViewHolder,则会调用onBindViewHolder()。
RecyclerView局部刷新的实现原理也是基于RecyclerView的回收机制,即能直接复用的ViewHolder就不调用onBindViewHolder()。
结论
- 在一些场景下,如界面初始化,滑动等,ListView和RecyclerView都能很好地工作,两者并没有很大的差异;
- 数据源频繁更新的场景,如弹幕等RecyclerView的优势会非常明显;
- 列表页展示界面,需要支持动画,或者频繁更新,局部刷新,建议使用RecyclerView,更加强大完善,易扩展;其他情况如果对列表没有很大的要求,ListView在使用上会更加方便,快捷。
网友评论