Android RecyclerView工作原理分析.docx
- 文档编号:25413861
- 上传时间:2023-06-08
- 格式:DOCX
- 页数:34
- 大小:246.59KB
Android RecyclerView工作原理分析.docx
《Android RecyclerView工作原理分析.docx》由会员分享,可在线阅读,更多相关《Android RecyclerView工作原理分析.docx(34页珍藏版)》请在冰豆网上搜索。
AndroidRecyclerView工作原理分析
AndroidRecyclerView工作原理分析
基本使用
RecyclerView的基本使用并不复杂,只需要提供一个RecyclerView.Apdater的实现用于处理数据集与ItemView的绑定关系,和一个RecyclerView.LayoutManager的实现用于测量并布局ItemView。
绘制流程
众所周知,Android控件的绘制可以分为3个步骤:
measure、layout、draw。
RecyclerView的绘制自然也经这3个步骤。
但是,RecyclerView将它的measure与layout过程委托给了RecyclerView.LayoutManager来处理,并且,它对子控件的measure及layout过程是逐个处理的,也就是说,执行完成一个子控件的measure及layout过程再去执行下一个。
下面看下这段代码:
protectedvoidonMeasure(intwidthSpec,intheightSpec){
...
if(mLayout.mAutoMeasure){
finalintwidthMode=MeasureSpec.getMode(widthSpec);
finalintheightMode=MeasureSpec.getMode(heightSpec);
finalbooleanskipMeasure=widthMode==MeasureSpec.EXACTLY
&&heightMode==MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler,mState,widthSpec,heightSpec);
if(skipMeasure||mAdapter==null){
return;
}
...
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec,heightSpec);
...
}else{
...
}
}
这是RecyclerView的测量方法,再看下dispatchLayoutStep2()方法:
privatevoiddispatchLayoutStep2(){
...
mLayout.onLayoutChildren(mRecycler,mState);
...
}
上面的mLayout就是一个RecyclerView.LayoutManager实例。
通过以上代码(和方法名称),不难推断出,RecyclerView的measure及layout过程委托给了RecyclerView.LayoutManager。
接着看onLayoutChildren方法,在兼容包中提供了3个RecyclerView.LayoutManager的实现,这里我就只以LinearLayoutManager来举例说明:
publicvoidonLayoutChildren(RecyclerView.Recyclerrecycler,RecyclerView.Statestate){
//layoutalgorithm:
//1)bycheckingchildrenandothervariables,findananchorcoordinateandananchor
//itemposition.
//2)filltowardsstart,stackingfrombottom
//3)filltowardsend,stackingfromtop
//4)scrolltofulfillrequirementslikestackfrombottom.
...
mAnchorInfo.mLayoutFromEnd=mShouldReverseLayout^mStackFromEnd;
//calculateanchorpositionandcoordinate
updateAnchorInfoForLayout(recycler,state,mAnchorInfo);
...
if(mAnchorInfo.mLayoutFromEnd){
...
}else{
//filltowardsend
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra=extraForEnd;
fill(recycler,mLayoutState,state,false);
endOffset=mLayoutState.mOffset;
finalintlastElement=mLayoutState.mCurrentPosition;
if(mLayoutState.mAvailable>0){
extraForStart+=mLayoutState.mAvailable;
}
//filltowardsstart
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra=extraForStart;
mLayoutState.mCurrentPosition+=mLayoutState.mItemDirection;
fill(recycler,mLayoutState,state,false);
startOffset=mLayoutState.mOffset;
...
}
...
}
源码中的注释部分我并没有略去,它已经解释了此处的逻辑了。
这里我以垂直布局来说明,mAnchorInfo为布局锚点信息,包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)——这里是指start、end方向。
这部分代码的功能就是:
确定布局锚点,以此为起点向开始和结束方向填充ItemView,如图所示:
在上一段代码中,fill()方法的作用就是填充ItemView,而图(3)说明了,在上段代码中fill()方法调用2次的原因。
虽然图(3)是更为普遍的情况,而且在实现填充ItemView算法时,也是按图(3)所示来实现的,但是mAnchorInfo在赋值过程(updateAnchorInfoForLayout)中,只会出现图
(1)、图
(2)所示情况。
现在来看下fill()方法:
intfill(RecyclerView.Recyclerrecycler,LayoutStatelayoutState,
RecyclerView.Statestate,booleanstopOnFocusable){
...
intremainingSpace=layoutState.mAvailable+layoutState.mExtra;
LayoutChunkResultlayoutChunkResult=newLayoutChunkResult();
while(...&&layoutState.hasMore(state)){
...
layoutChunk(recycler,state,layoutState,layoutChunkResult);
...
if(...){
layoutState.mAvailable-=layoutChunkResult.mConsumed;
remainingSpace-=layoutChunkResult.mConsumed;
}
if(layoutState.mScrollingOffset!
=LayoutState.SCOLLING_OFFSET_NaN){
layoutState.mScrollingOffset+=layoutChunkResult.mConsumed;
if(layoutState.mAvailable<0){
layoutState.mScrollingOffset+=layoutState.mAvailable;
}
recycleByLayoutState(recycler,layoutState);
}
}
...
}
下面是layoutChunk()方法:
voidlayoutChunk(RecyclerView.Recyclerrecycler,RecyclerView.Statestate,
LayoutStatelayoutState,LayoutChunkResultresult){
Viewview=layoutState.next(recycler);
...
if(layoutState.mScrapList==null){
if(mShouldReverseLayout==(layoutState.mLayoutDirection
==LayoutState.LAYOUT_START)){
addView(view);
}else{
addView(view,0);
}
}
...
measureChildWithMargins(view,0,0);
...
//WecalculateeverythingwithView'sboundingbox(whichincludesdecorandmargins)
//Tocalculatecorrectlayoutposition,wesubtractmargins.
layoutDecorated(view,left+params.leftMargin,top+params.topMargin,
right-params.rightMargin,bottom-params.bottomMargin);
...
}
这里的addView()方法,其实就是ViewGroup的addView()方法;measureChildWithMargins()方法看名字就知道是用于测量子控件大小的,这里我先跳过这个方法的解释,放在后面来做,目前就简单地理解为测量子控件大小就好了。
下面是layoutDecoreated()方法:
publicvoidlayoutDecorated(...){
...
child.layout(...);
}
总结上面代码,在RecyclerView的measure及layout阶段,填充ItemView的算法为:
向父容器增加子控件,测量子控件大小,布局子控件,布局锚点向当前布局方向平移子控件大小,重复上诉步骤至RecyclerView可绘制空间消耗完毕或子控件已全部填充。
这样所有的子控件的measure及layout过程就完成了。
回到RecyclerView的onMeasure方法,执行mLayout.setMeasuredDimensionFromChildren(widthSpec,heightSpec)这行代码的作用就是根据子控件的大小,设置RecyclerView的大小。
至此,RecyclerView的measure和layout实际上已经完成了。
但是,你有可能已经发现上面过程中的问题了:
如何确定RecyclerView的可绘制空间?
不过,如果你熟悉android控件的绘制机制的话,这就不是问题。
其实,这里的可绘制空间,可以简单地理解为父容器的大小;更准确的描述是,父容器对RecyclerView的布局大小的要求,可以通过MeasureSpec.getSize()方法获得——这里不包括滑动情况,滑动情况会在后文描述。
需要特别说明的是在23.2.0版本之前,RecyclerView是不支持WRAP_CONTENT的。
先看下RecyclerView的onLayout()方法:
protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){
...
dispatchLayout();
...
}
这是dispatchLayout()方法:
voiddispatchLayout(){
...
if(mState.mLayoutStep==State.STEP_START){
dispatchLayoutStep1();
...
dispatchLayoutStep2();
}
dispatchLayoutStep3();
...
}
可以看出,这里也会执行子控件的measure及layout过程。
结合onMeasure方法对skipMeasure的判断可以看出,如果要支持WRAP_CONTENT,那么子控件的measure及layout就会提前在RecyclerView的测量方法中执行完成,也就是说,先确定了子控件的大小及位置后,再由此设置RecyclerView的大小;如果是其它情况(测量模式为EXACTLY),子控件的measure及layout过程就会延迟至RecyclerView的layout过程(RecyclerView.onLayout())中执行。
再看onMeasure方法中的mLayout.mAutoMeasure,它表示,RecyclerView的measure及layout过程是否要委托给RecyclerView.LayoutManager,在兼容包中提供的3种RecyclerView.LayoutManager的这个属性默认都是为true的。
好了,以上就是RecyclerView的measure及layout过程,下面来看下它的draw过程。
RecyclerView的draw过程可以分为2部分来看:
RecyclerView负责绘制所有decoration;ItemView的绘制由ViewGroup处理,这里的绘制是android常规绘制逻辑,本文就不再阐述了。
下面来看看RecyclerView的draw()和onDraw()方法:
@Override
publicvoiddraw(Canvasc){
super.draw(c);
finalintcount=mItemDecorations.size();
for(inti=0;i mItemDecorations.get(i).onDrawOver(c,this,mState); } ... } @Override publicvoidonDraw(Canvasc){ super.onDraw(c); finalintcount=mItemDecorations.size(); for(inti=0;i mItemDecorations.get(i).onDraw(c,this,mState); } } 可以看出对于decoration的绘制代码上十分简单。 但是这里,我必须要抱怨一下RecyclerView.ItemDecoration的设计,它实在是太过于灵活了,虽然理论上我们可以使用它在RecyclerView内的任何地方绘制你想要的任何东西——到这一步,RecyclerView的大小位置已经确定的哦。 但是过于灵活,太难使用,以至往往使我们无从下手。 好了,题外话就不多说了,来看看decoration的绘制吧。 还记得上面提到过的measureChildWithMargins()方法吗? 先来看看它: publicvoidmeasureChildWithMargins(Viewchild,intwidthUsed,intheightUsed){ finalLayoutParamslp=(LayoutParams)child.getLayoutParams(); finalRectinsets=mRecyclerView.getItemDecorInsetsForChild(child); widthUsed+=insets.left+insets.right; heightUsed+=insets.top+insets.bottom; finalintwidthSpec=... finalintheightSpec=... if(shouldMeasureChild(child,widthSpec,heightSpec,lp)){ child.measure(widthSpec,heightSpec); } } 这里是getItemDecorInsetsForChild()方法: RectgetItemDecorInsetsForChild(Viewchild){ ... finalRectinsets=lp.mDecorInsets; insets.set(0,0,0,0); finalintdecorCount=mItemDecorations.size(); for(inti=0;i mTempRect.set(0,0,0,0); mItemDecorations.get(i).getItemOffsets(mTempRect,child,this,mState); insets.left+=mTempRect.left; insets.top+=mTempRect.top; insets.right+=mTempRect.right; insets.bottom+=mTempRect.bottom; } lp.mInsetsDirty=false; returninsets; } 方法getItemOffsets()就是我们在实现一个RecyclerView.ItemDecoration时可以重写的方法,通过mTempRect的大小,可以为每个ItemView设置位置偏移量,这个偏移量最终会参与计算ItemView的大小,也就是说ItemView的大小是包含这个位置偏移量的。 我们在重写getItemOffsets()时,可以指定任意数值的偏移量: 4个方向的位置偏移量对应mTempRect的4个属性(left,top,right,bottom),我以topoffset的值在垂直线性布局中的应用来举例说明下。 如果topoffset等于0,那么ItemView之间就没有空隙;如果topoffset大于0,那么ItemView之前就会有一个间隙;如果topoffset小于0,那么ItemView之间就会有重叠的区域。 当然,我们在实现RecyclerView.ItemDecoration时,并不一定要重写getItemOffsets(),同样的对于RecyclerView.ItemDecoration.onDraw()或RecyclerView.ItemDecoration.onDrawOver()方法也不是一定要重写,而且,这个绘制方法和我们所设置的位置偏移量没有任何联系。 下面我来实现一个RecyclerView.ItemDecoration来加深下这里的理解: 我将在垂直线性布局下,在ItemView间绘制一条5个像素宽、只有ItemView一半长、与ItemView居中对齐的红色分割线,这条分割线在ItemView内部top位置。 @Override publicvoidonDraw(Canvasc,RecyclerViewparent,RecyclerView.Statestate){ Paintpaint=newPaint(); paint.setColor(Color.RED); for(inti=0;i finalViewchild=parent.getChildAt(i); floatleft=child.getLeft()+(child.getRight()-child.getLeft())/4; floattop=child.getTop(); floatright=left+(child.getRight()-child.getLeft())/2; floatbottom=top+5; c.drawRect(left,top,right,bottom,paint); } } @Override publicvoidgetItemOffsets(RectoutRect,Viewview,RecyclerViewparent,RecyclerView.Statestate){ outRect.set(0,0,0,0); } 代码不是很严谨,大家姑且一看吧,当然这里getItemOffsets()方法可以省略的。 以上就是RecyclerView的整个绘制流程了,值得注意的地方也就是在23.2.0中RecyclerView支持WRAP_CONTENT属性了;还有就是ItemView的填充算法fill()算是一个亮点吧。 接下来,我将分析ReyclerView的滑动流程。 滑动 RecyclerView的滑动过程可以分为2个阶段: 手指在屏幕上移动,使RecyclerView滑动的过程,可以称为scroll;手指离开屏幕,RecyclerView继续滑动一段距离的过程,可以称为fling。 现在先看看RecyclerView的触屏事件处理onTouchEvent()方法: publicbooleanonTouchEvent(MotionEvente){ ... if(mVelocityTracker==null){ mVelocityTracker=VelocityTracker.obtain(); } ... switch(action){ ... caseMotionEvent.ACTION_MOVE: { ... finalintx=(int)(MotionEventCompat.getX(e,index)+0.5f); finalinty=(int)(MotionEventCompat.getY(e,index)+0.5f); intdx=mLastTouchX-x; intd
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Android RecyclerView工作原理分析 RecyclerView 工作 原理 分析