欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-12-20 14:50:58
前言 前几篇文章,笔者分别讲述了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)全面解析

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

上一篇:

下一篇: