Android RecyclerView工作原理分析.docx

上传人:b****7 文档编号:25413861 上传时间:2023-06-08 格式:DOCX 页数:34 大小:246.59KB
下载 相关 举报
Android RecyclerView工作原理分析.docx_第1页
第1页 / 共34页
Android RecyclerView工作原理分析.docx_第2页
第2页 / 共34页
Android RecyclerView工作原理分析.docx_第3页
第3页 / 共34页
Android RecyclerView工作原理分析.docx_第4页
第4页 / 共34页
Android RecyclerView工作原理分析.docx_第5页
第5页 / 共34页
点击查看更多>>
下载资源
资源描述

Android RecyclerView工作原理分析.docx

《Android RecyclerView工作原理分析.docx》由会员分享,可在线阅读,更多相关《Android RecyclerView工作原理分析.docx(34页珍藏版)》请在冰豆网上搜索。

Android RecyclerView工作原理分析.docx

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

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 建筑土木

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1