Android View 绘制流程(Draw)全面解析
前言
前几篇文章,笔者分别讲述了DecorView,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程——绘制流程。测量流程决定了View的大小,布局流程决定了View的位置,那么绘制流程将决定View的样子,一个View该显示什么由绘制流程完成。以下源码均取自AndroidAPI21。
从performDraw说起
前面几篇文章提到,三大工作流程始于ViewRootImpl#performTraversals,在这个方法内部会分别调用performMeasure,performLayout,performDraw三个方法来分别完成测量,布局,绘制流程。那么我们现在先从performDraw方法看起,ViewRootImpl#performDraw:
privatevoidperformDraw(){
//...
finalbooleanfullRedrawNeeded=mFullRedrawNeeded;
try{
draw(fullRedrawNeeded);
}finally{
mIsDrawing=false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
//省略...
}
里面又调用了ViewRootImpl#draw方法,并传递了fullRedrawNeeded参数,而该参数由mFullRedrawNeeded成员变量获取,它的作用是判断是否需要重新绘制全部视图,如果是第一次绘制视图,那么显然应该绘制所以的视图,如果由于某些原因,导致了视图重绘,那么就没有必要绘制所有视图。我们来看看ViewRootImpl#draw:
privatevoiddraw(booleanfullRedrawNeeded){
...
//获取mDirty,该值表示需要重绘的区域
finalRectdirty=mDirty;
if(mSurfaceHolder!=null){
//Theappownsthesurface,wewon'tdraw.
dirty.setEmpty();
if(animating){
if(mScroller!=null){
mScroller.abortAnimation();
}
disposeResizeBuffer();
}
return;
}
//如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制
//第一次绘制流程,需要绘制所有视图
if(fullRedrawNeeded){
mAttachInfo.mIgnoreDirtyState=true;
dirty.set(0,0,(int)(mWidth*appScale+0.5f),(int)(mHeight*appScale+0.5f));
}
//省略...
if(!drawSoftware(surface,mAttachInfo,xOffset,yOffset,scalingRequired,dirty)){
return;
}
}
这里省略了一部分代码,我们只看关键代码,首先是先获取了mDirty值,该值保存了需要重绘的区域的信息,关于视图重绘,后面会有文章专门叙述,这里先熟悉一下。接着根据fullRedrawNeeded来判断是否需要重置dirty区域,最后调用了ViewRootImpl#drawSoftware方法,并把相关参数传递进去,包括dirty区域,我们接着看该方法的源码:
privatebooleandrawSoftware(Surfacesurface,AttachInfoattachInfo,intxoff,intyoff,
booleanscalingRequired,Rectdirty){
//Drawwithsoftwarerenderer.
finalCanvascanvas;
try{
finalintleft=dirty.left;
finalinttop=dirty.top;
finalintright=dirty.right;
finalintbottom=dirty.bottom;
//锁定canvas区域,由dirty区域决定
canvas=mSurface.lockCanvas(dirty);
//ThedirtyrectanglecanbemodifiedbySurface.lockCanvas()
//noinspectionConstantConditions
if(left!=dirty.left||top!=dirty.top||right!=dirty.right
||bottom!=dirty.bottom){
attachInfo.mIgnoreDirtyState=true;
}
canvas.setDensity(mDensity);
}
try{
if(!canvas.isOpaque()||yoff!=0||xoff!=0){
canvas.drawColor(0,PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating=false;
attachInfo.mDrawingTime=SystemClock.uptimeMillis();
mView.mPrivateFlags|=View.PFLAG_DRAWN;
try{
canvas.translate(-xoff,-yoff);
if(mTranslator!=null){
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired?mNoncompatDensity:0);
attachInfo.mSetIgnoreDirtyState=false;
//正式开始绘制
mView.draw(canvas);
}
}
returntrue;
}
可以看书,首先是实例化了Canvas对象,然后锁定该canvas的区域,由dirty区域决定,接着对canvas进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,前面分析过,mView就是DecorView,也就是说从DecorView开始绘制,前面所做的一切工作都是准备工作,而现在则是正式开始绘制流程。
View的绘制
由于ViewGroup没有重写draw方法,因此所有的View都是调用View#draw方法,因此,我们直接看它的源码:
publicvoiddraw(Canvascanvas){
finalintprivateFlags=mPrivateFlags;
finalbooleandirtyOpaque=(privateFlags&PFLAG_DIRTY_MASK)==PFLAG_DIRTY_OPAQUE&&
(mAttachInfo==null||!mAttachInfo.mIgnoreDirtyState);
mPrivateFlags=(privateFlags&~PFLAG_DIRTY_MASK)|PFLAG_DRAWN;
/*
*Drawtraversalperformsseveraldrawingstepswhichmustbeexecuted
*intheappropriateorder:
*
*1.Drawthebackground
*2.Ifnecessary,savethecanvas'layerstoprepareforfading
*3.Drawview'scontent
*4.Drawchildren
*5.Ifnecessary,drawthefadingedgesandrestorelayers
*6.Drawdecorations(scrollbarsforinstance)
*/
//Step1,drawthebackground,ifneeded
intsaveCount;
if(!dirtyOpaque){
drawBackground(canvas);
}
//skipstep2&5ifpossible(commoncase)
finalintviewFlags=mViewFlags;
booleanhorizontalEdges=(viewFlags&FADING_EDGE_HORIZONTAL)!=0;
booleanverticalEdges=(viewFlags&FADING_EDGE_VERTICAL)!=0;
if(!verticalEdges&&!horizontalEdges){
//Step3,drawthecontent
if(!dirtyOpaque)onDraw(canvas);
//Step4,drawthechildren
dispatchDraw(canvas);
//OverlayispartofthecontentanddrawsbeneathForeground
if(mOverlay!=null&&!mOverlay.isEmpty()){
mOverlay.getOverlayView().dispatchDraw(canvas);
}
//Step6,drawdecorations(foreground,scrollbars)
onDrawForeground(canvas);
//we'redone...
return;
}
...
}
可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个View既然是透明的,那就没必要绘制它了。接着是绘制流程的六个步骤,这里先小结这六个步骤分别是什么,然后再展开来讲。
绘制流程的六个步骤:
1、对View的背景进行绘制
2、保存当前的图层信息(可跳过)
3、绘制View的内容
4、对View的子View进行绘制(如果有子View)
5、绘制View的褪色的边缘,类似于阴影效果(可跳过)
6、绘制View的装饰(例如:滚动条)
其中第2步和第5步是可以跳过的,我们这里不做分析,我们重点来分析其它步骤。
Skip1:绘制背景
这里调用了View#drawBackground方法,我们看它的源码:
privatevoiddrawBackground(Canvascanvas){
//mBackground是该View的背景参数,比如背景颜色
finalDrawablebackground=mBackground;
if(background==null){
return;
}
//根据View四个布局参数来确定背景的边界
setBackgroundBounds();
...
//获取当前View的mScrollX和mScrollY值
finalintscrollX=mScrollX;
finalintscrollY=mScrollY;
if((scrollX|scrollY)==0){
background.draw(canvas);
}else{
//如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
canvas.translate(scrollX,scrollY);
background.draw(canvas);
canvas.translate(-scrollX,-scrollY);
}
}
可以看出,这里考虑到了view的偏移参数,scrollX和scrollY,绘制背景在偏移后的view中绘制。
Skip3:绘制内容
这里调用了View#onDraw方法,View中该方法是一个空实现,因为不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。
Skip4:绘制子View
如果当前的View是一个ViewGroup类型,那么就需要绘制它的子View,这里调用了dispatchDraw,而View中该方法是空实现,实际是ViewGroup重写了这个方法,那么我们来看看,ViewGroup#dispatchDraw:
protectedvoiddispatchDraw(Canvascanvas){
booleanusingRenderNodeProperties=canvas.isRecordingFor(mRenderNode);
finalintchildrenCount=mChildrenCount;
finalView[]children=mChildren;
intflags=mGroupFlags;
for(inti=0;i<childrenCount;i++){
while(transientIndex>=0&&mTransientIndices.get(transientIndex)==i){
finalViewtransientChild=mTransientViews.get(transientIndex);
if((transientChild.mViewFlags&VISIBILITY_MASK)==VISIBLE||
transientChild.getAnimation()!=null){
more|=drawChild(canvas,transientChild,drawingTime);
}
transientIndex++;
if(transientIndex>=transientCount){
transientIndex=-1;
}
}
intchildIndex=customOrder?getChildDrawingOrder(childrenCount,i):i;
finalViewchild=(preorderedList==null)
?children[childIndex]:preorderedList.get(childIndex);
if((child.mViewFlags&VISIBILITY_MASK)==VISIBLE||child.getAnimation()!=null){
more|=drawChild(canvas,child,drawingTime);
}
}
//省略...
}
源码很长,这里简单说明一下,里面主要遍历了所以子View,每个子View都调用了drawChild这个方法,我们找到这个方法,ViewGroup#drawChild:
protectedbooleandrawChild(Canvascanvas,Viewchild,longdrawingTime){
returnchild.draw(canvas,this,drawingTime);
}
可以看出,这里调用了View的draw方法,但这个方法并不是上面所说的,因为参数不同,我们来看看这个方法,View#draw:
booleandraw(Canvascanvas,ViewGroupparent,longdrawingTime){
//省略...
if(!drawingWithDrawingCache){
if(drawingWithRenderNode){
mPrivateFlags&=~PFLAG_DIRTY_MASK;
((DisplayListCanvas)canvas).drawRenderNode(renderNode);
}else{
//Fastpathforlayoutswithnobackgrounds
if((mPrivateFlags&PFLAG_SKIP_DRAW)==PFLAG_SKIP_DRAW){
mPrivateFlags&=~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
}else{
draw(canvas);
}
}
}elseif(cache!=null){
mPrivateFlags&=~PFLAG_DIRTY_MASK;
if(layerType==LAYER_TYPE_NONE){
//nolayerpaint,usetemporarypainttodrawbitmap
PaintcachePaint=parent.mCachePaint;
if(cachePaint==null){
cachePaint=newPaint();
cachePaint.setDither(false);
parent.mCachePaint=cachePaint;
}
cachePaint.setAlpha((int)(alpha*255));
canvas.drawBitmap(cache,0.0f,0.0f,cachePaint);
}else{
//uselayerpainttodrawthebitmap,mergingthetwoalphas,butalsorestore
intlayerPaintAlpha=mLayerPaint.getAlpha();
mLayerPaint.setAlpha((int)(alpha*layerPaintAlpha));
canvas.drawBitmap(cache,0.0f,0.0f,mLayerPaint);
mLayerPaint.setAlpha(layerPaintAlpha);
}
}
}
我们主要来看核心部分,首先判断是否已经有缓存,即之前是否已经绘制过一次了,如果没有,则会调用draw(canvas)方法,开始正常的绘制,即上面所说的六个步骤,否则利用缓存来显示。
这一步也可以归纳为ViewGroup绘制过程,它对子View进行了绘制,而子View又会调用自身的draw方法来绘制自身,这样不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制。
Skip6绘制装饰
所谓的绘制装饰,就是指View除了背景、内容、子View的其余部分,例如滚动条等,我们看View#onDrawForeground:
publicvoidonDrawForeground(Canvascanvas){
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
finalDrawableforeground=mForegroundInfo!=null?mForegroundInfo.mDrawable:null;
if(foreground!=null){
if(mForegroundInfo.mBoundsChanged){
mForegroundInfo.mBoundsChanged=false;
finalRectselfBounds=mForegroundInfo.mSelfBounds;
finalRectoverlayBounds=mForegroundInfo.mOverlayBounds;
if(mForegroundInfo.mInsidePadding){
selfBounds.set(0,0,getWidth(),getHeight());
}else{
selfBounds.set(getPaddingLeft(),getPaddingTop(),
getWidth()-getPaddingRight(),getHeight()-getPaddingBottom());
}
finalintld=getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity,foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(),selfBounds,overlayBounds,ld);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
可以看出,逻辑很清晰,和一般的绘制流程非常相似,都是先设定绘制区域,然后利用canvas进行绘制,这里就不展开详细地说了,有兴趣的可以继续了解下去。
那么,到目前为止,View的绘制流程也讲述完毕了,希望这篇文章对你们起到帮助作用,谢谢你们的阅读。
更多阅读
AndroidView测量流程(Measure)全面解析
AndroidView布局流程(Layout)全面解析
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。