当前位置: 移动技术网 > 移动技术>移动开发>Android > Android View 绘制流程(Draw)全面解析

Android View 绘制流程(Draw)全面解析

2019年07月24日  | 移动技术网移动技术  | 我要评论
前言 前几篇文章,笔者分别讲述了decorview,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程——绘制流程。测量流程决定了view的大

前言

前几篇文章,笔者分别讲述了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)全面解析

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网