Android View 绘制流程(Draw)全面解析
前言
前几篇文章,笔者分别讲述了decorview,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程——绘制流程。测量流程决定了view的大小,布局流程决定了view的位置,那么绘制流程将决定view的样子,一个view该显示什么由绘制流程完成。以下源码均取自android api 21。
从performdraw说起
前面几篇文章提到,三大工作流程始于viewrootimpl#performtraversals,在这个方法内部会分别调用performmeasure,performlayout,performdraw三个方法来分别完成测量,布局,绘制流程。那么我们现在先从performdraw方法看起,viewrootimpl#performdraw:
private void performdraw() { //... final boolean fullredrawneeded = mfullredrawneeded; try { draw(fullredrawneeded); } finally { misdrawing = false; trace.traceend(trace.trace_tag_view); } //省略... }
里面又调用了viewrootimpl#draw方法,并传递了fullredrawneeded参数,而该参数由mfullredrawneeded成员变量获取,它的作用是判断是否需要重新绘制全部视图,如果是第一次绘制视图,那么显然应该绘制所以的视图,如果由于某些原因,导致了视图重绘,那么就没有必要绘制所有视图。我们来看看viewrootimpl#draw:
private void draw(boolean fullredrawneeded) { ... //获取mdirty,该值表示需要重绘的区域 final rect dirty = mdirty; if (msurfaceholder != null) { // the app owns the surface, we won't draw. 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区域,我们接着看该方法的源码:
private boolean drawsoftware(surface surface, attachinfo attachinfo, int xoff, int yoff, boolean scalingrequired, rect dirty) { // draw with software renderer. final canvas canvas; try { final int left = dirty.left; final int top = dirty.top; final int right = dirty.right; final int bottom = dirty.bottom; //锁定canvas区域,由dirty区域决定 canvas = msurface.lockcanvas(dirty); // the dirty rectangle can be modified by surface.lockcanvas() //noinspection constantconditions 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); } } return true; }
可以看书,首先是实例化了canvas对象,然后锁定该canvas的区域,由dirty区域决定,接着对canvas进行一系列的属性赋值,最后调用了mview.draw(canvas)方法,前面分析过,mview就是decorview,也就是说从decorview开始绘制,前面所做的一切工作都是准备工作,而现在则是正式开始绘制流程。
view的绘制
由于viewgroup没有重写draw方法,因此所有的view都是调用view#draw方法,因此,我们直接看它的源码:
public void draw(canvas canvas) { final int privateflags = mprivateflags; final boolean dirtyopaque = (privateflags & pflag_dirty_mask) == pflag_dirty_opaque && (mattachinfo == null || !mattachinfo.mignoredirtystate); mprivateflags = (privateflags & ~pflag_dirty_mask) | pflag_drawn; /* * draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. draw the background * 2. if necessary, save the canvas' layers to prepare for fading * 3. draw view's content * 4. draw children * 5. if necessary, draw the fading edges and restore layers * 6. draw decorations (scrollbars for instance) */ // step 1, draw the background, if needed int savecount; if (!dirtyopaque) { drawbackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewflags = mviewflags; boolean horizontaledges = (viewflags & fading_edge_horizontal) != 0; boolean verticaledges = (viewflags & fading_edge_vertical) != 0; if (!verticaledges && !horizontaledges) { // step 3, draw the content if (!dirtyopaque) ondraw(canvas); // step 4, draw the children dispatchdraw(canvas); // overlay is part of the content and draws beneath foreground if (moverlay != null && !moverlay.isempty()) { moverlay.getoverlayview().dispatchdraw(canvas); } // step 6, draw decorations (foreground, scrollbars) ondrawforeground(canvas); // we're done... return; } ... }
可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。我们首先来看一开始的标记位dirtyopaque,该标记位的作用是判断当前view是否是透明的,如果view是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个view既然是透明的,那就没必要绘制它了。接着是绘制流程的六个步骤,这里先小结这六个步骤分别是什么,然后再展开来讲。
绘制流程的六个步骤:
1、对view的背景进行绘制
2、保存当前的图层信息(可跳过)
3、绘制view的内容
4、对view的子view进行绘制(如果有子view)
5、绘制view的褪色的边缘,类似于阴影效果(可跳过)
6、绘制view的装饰(例如:滚动条)
其中第2步和第5步是可以跳过的,我们这里不做分析,我们重点来分析其它步骤。
skip 1:绘制背景
这里调用了view#drawbackground方法,我们看它的源码:
private void drawbackground(canvas canvas) { //mbackground是该view的背景参数,比如背景颜色 final drawable background = mbackground; if (background == null) { return; } //根据view四个布局参数来确定背景的边界 setbackgroundbounds(); ... //获取当前view的mscrollx和mscrolly值 final int scrollx = mscrollx; final int scrolly = 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中绘制。
skip 3:绘制内容
这里调用了view#ondraw方法,view中该方法是一个空实现,因为不同的view有着不同的内容,这需要我们自己去实现,即在自定义view中重写该方法来实现。
skip 4: 绘制子view
如果当前的view是一个viewgroup类型,那么就需要绘制它的子view,这里调用了dispatchdraw,而view中该方法是空实现,实际是viewgroup重写了这个方法,那么我们来看看,viewgroup#dispatchdraw:
protected void dispatchdraw(canvas canvas) { boolean usingrendernodeproperties = canvas.isrecordingfor(mrendernode); final int childrencount = mchildrencount; final view[] children = mchildren; int flags = mgroupflags; for (int i = 0; i < childrencount; i++) { while (transientindex >= 0 && mtransientindices.get(transientindex) == i) { final view transientchild = mtransientviews.get(transientindex); if ((transientchild.mviewflags & visibility_mask) == visible || transientchild.getanimation() != null) { more |= drawchild(canvas, transientchild, drawingtime); } transientindex++; if (transientindex >= transientcount) { transientindex = -1; } } int childindex = customorder ? getchilddrawingorder(childrencount, i) : i; final view child = (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:
protected boolean drawchild(canvas canvas, view child, long drawingtime) { return child.draw(canvas, this, drawingtime); }
可以看出,这里调用了view的draw方法,但这个方法并不是上面所说的,因为参数不同,我们来看看这个方法,view#draw:
boolean draw(canvas canvas, viewgroup parent, long drawingtime) { //省略... if (!drawingwithdrawingcache) { if (drawingwithrendernode) { mprivateflags &= ~pflag_dirty_mask; ((displaylistcanvas) canvas).drawrendernode(rendernode); } else { // fast path for layouts with no backgrounds if ((mprivateflags & pflag_skip_draw) == pflag_skip_draw) { mprivateflags &= ~pflag_dirty_mask; dispatchdraw(canvas); } else { draw(canvas); } } } else if (cache != null) { mprivateflags &= ~pflag_dirty_mask; if (layertype == layer_type_none) { // no layer paint, use temporary paint to draw bitmap paint cachepaint = parent.mcachepaint; if (cachepaint == null) { cachepaint = new paint(); cachepaint.setdither(false); parent.mcachepaint = cachepaint; } cachepaint.setalpha((int) (alpha * 255)); canvas.drawbitmap(cache, 0.0f, 0.0f, cachepaint); } else { // use layer paint to draw the bitmap, merging the two alphas, but also restore int layerpaintalpha = 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树完成绘制。
skip 6 绘制装饰
所谓的绘制装饰,就是指view除了背景、内容、子view的其余部分,例如滚动条等,我们看view#ondrawforeground:
public void ondrawforeground(canvas canvas) { ondrawscrollindicators(canvas); ondrawscrollbars(canvas); final drawable foreground = mforegroundinfo != null ? mforegroundinfo.mdrawable : null; if (foreground != null) { if (mforegroundinfo.mboundschanged) { mforegroundinfo.mboundschanged = false; final rect selfbounds = mforegroundinfo.mselfbounds; final rect overlaybounds = mforegroundinfo.moverlaybounds; if (mforegroundinfo.minsidepadding) { selfbounds.set(0, 0, getwidth(), getheight()); } else { selfbounds.set(getpaddingleft(), getpaddingtop(), getwidth() - getpaddingright(), getheight() - getpaddingbottom()); } final int ld = getlayoutdirection(); gravity.apply(mforegroundinfo.mgravity, foreground.getintrinsicwidth(), foreground.getintrinsicheight(), selfbounds, overlaybounds, ld); foreground.setbounds(overlaybounds); } foreground.draw(canvas); } }
可以看出,逻辑很清晰,和一般的绘制流程非常相似,都是先设定绘制区域,然后利用canvas进行绘制,这里就不展开详细地说了,有兴趣的可以继续了解下去。
那么,到目前为止,view的绘制流程也讲述完毕了,希望这篇文章对你们起到帮助作用,谢谢你们的阅读。
更多阅读
android view 测量流程(measure)全面解析
android view 布局流程(layout)全面解析
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。