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;imItemDecorations.get(i).onDrawOver(c,this,mState);
}
...
}
@Override
publicvoidonDraw(Canvasc){
super.onDraw(c);
finalintcount=mItemDecorations.size();
for(inti=0;imItemDecorations.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;imTempRect.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;ifinalViewchild=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