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)全面解析

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

(0)

相关推荐

  • 深入理解Android中View绘制的三大流程

    前言 最近对Android中View的绘制机制有了一些新的认识,所以想记录下来并分享给大家.View的工作流程主要是指measure.layout.draw这三大流程,即测量.布局和绘制,其中measure确定View的测量宽高,layout根据测量的宽高确定View在其父View中的四个顶点的位置,而draw则将View绘制到屏幕上,这样通过ViewGroup的递归遍历,一个View树就展现在屏幕上了. 说的简单,下面带大家一步一步从源码中分析: Android的View是树形结构的: 基本概

  • Android视图的绘制流程(上) View的测量

    综述 View的绘制流程可以分为三大步,它们分别是measure,layout和draw过程.measure表示View的测量过程,用于测量View的宽度和高度:layout用于确定View在父容器的位置:draw则是负责将View绘制到屏幕中.下面主要来看一下View的Measure过程. 测量过程 View的绘制流程是从ViewRoot的performTraversals方法开始的,ViewRoot对应ViewRootImpl类.ViewRoot在performTraversals中会调用p

  • Android使用自定义View绘制渐隐渐现动画

    实现了一个有趣的小东西:使用自定义View绘图,一边画线,画出的线条渐渐变淡,直到消失.效果如下图所示: 用属性动画或者渐变填充(Shader)可以做到一笔一笔的变化,但要想一笔渐变(手指不抬起边画边渐隐),没在Android中找到现成的API可用.所以,自己做了一个. 基本的想法是这样的: 在View的onTouchEvent中记录触摸点,生成一条一条的线LineElement,放在一个List中.给每个LineElement配置一个Paint实例. 在onDraw中绘制线段. 变换LineE

  • 浅谈Android View绘制三大流程探索及常见问题

    View绘制的三大流程,指的是measure(测量).layout(布局).draw(绘制) measure负责确定View的测量宽/高,也就是该View需要占用屏幕的大小,确定完View需要占用的屏幕大小后,就会通过layout确定View的最终宽/高和四个顶点在手机界面上的位置,等通过measure和layout过程确定了View的宽高和要显示的位置后,就会执行draw绘制View的内容到手机屏幕上. 在详细介绍这三大流程之前,需要简单了解一下ViewRootImpl,View绘制的三大步骤

  • Android自定义View实现绘制虚线的方法详解

    前言 说实话当第一次看到这个需求的时候,第一反应就是Canvas只有drawLine方法,并没有drawDashLine方法啊!这咋整啊,难道要我自己做个遍历不断的drawLine?不到1秒,我就放弃这个想法了,因为太恶心了.方法肯定是有的,只不过我不知道而已. 绘制方法 最简单的方法是利用ShapeDrawable,比如说你想用虚线要隔开两个控件,就可以在这两个控件中加个View,然后给它个虚线背景. 嗯,理论上就是这样子的,实现上也很简单. <!-- drawable 文件 --> <

  • Android应用开发中View绘制的一些优化点解析

    一个通常的错误观念就是使用基本的布局结构(例如:LinearLayout.FrameLayout等)能够在大多数情况下    产生高效率 的布局. 显然,你的应用程序里添加的每一个控件和每一个布局都需要初始化.布局(layout).    绘制 (drawing).举例来说:嵌入一个LinearLayout会产生一个太深的布局层次.更严重的是,嵌入几个使    用 layout_weight属性的LinearLayout 将会导致大量的开销,因为每个子视图都需要被测量两次.这是反复解析    布

  • Android View如何绘制

    上文说道了Android如何测量,但是一个漂亮的控件我只知道您长到哪儿,这当然不行.只需要简单重写OnDraw方法,并在Canvas(画布)对象上调用那根五颜六色的画笔就能够画出这控件"性感"的外表.那么View又是如何进行绘制了? 要了解View如何绘制,就需要了解canvas(画布)是什么?paint(画笔)能够做什么. Ⅰ.canvas就是表示一块画布,你可以在上面画你所朝思暮想的东西.当我们重写onDraw方法的时候,就能够拿到一个Canvas对象,这个就是你的舞台,画你所思所

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

    前言 前几篇文章,笔者分别讲述了DecorView,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程--绘制流程.测量流程决定了View的大小,布局流程决定了View的位置,那么绘制流程将决定View的样子,一个View该显示什么由绘制流程完成.以下源码均取自Android API 21. 从performDraw说起 前面几篇文章提到,三大工作流程始于ViewRootImpl#performTraversals,在这个方法内部会分别调用performMeasure

  • Android View 布局流程(Layout)全面解析

    前言 上一篇文章,笔者详细讲述了View三大工作流程的第一个,Measure流程,如果对测量流程还不熟悉的读者可以参考一下上一篇文章.测量流程主要是对View树进行测量,获取每一个View的测量宽高,那么有了测量宽高,就是要进行布局流程了,布局流程相对测量流程来说简单许多.那么我们开始对layout流程进行详细的解析. ViewGroup的布局流程 上一篇文章提到,三大流程始于ViewRootImpl#performTraversals方法,在该方法内通过调用performMeasure.per

  • Android View 测量流程(Measure)全面解析

    前言 上一篇文章,笔者主要讲述了DecorView以及ViewRootImpl相关的作用,这里回顾一下上一章所说的内容:DecorView是视图的顶级View,我们添加的布局文件是它的一个子布局,而ViewRootImpl则负责渲染视图,它调用了一个performTraveals方法使得ViewTree开始三大工作流程,然后使得View展现在我们面前.本篇文章主要内容是:详细讲述View的测量(Measure)流程,主要以源码的形式呈现,源码均取自Android API 21. 从ViewRoo

  • Android view绘制流程详解

    绘制流程 measure 流程测量出 View 的宽高尺寸. layout 流程确定 View 的位置及最终尺寸. draw 流程将 View 绘制在屏幕上. Measure 测量流程 系统是通过 MeasureSpec 测量 View 的,在了解测量过程之前一定要了解这个 MeasureSpec . MeasureSpec MeasureSpec 是一个 32 位的 int 值打包而来的,打包为 MeasureSpec 主要是为了避免过多的对象内存分配. 为了方便操作,MeasureSpec

  • Android中View绘制流程详细介绍

    创建Window Window即窗口,这个概念在AndroidFramework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象.在介绍这个类之前,我们先来看看究竟什么是窗口呢? 实际上,窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域.通常具备以下两个特点: 独立绘制,不与其它界面相互影响: 不会触发其它界面的输入事件: 在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的S

  • Android View 绘制机制的详解

    View 绘制机制一. View 树的绘图流程 当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw.整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数展开,该函数所做 的工作可简单概况为是否需要重新计算视图大小(measure).是否需要重新安置视图的位置(layout).以及是否需要重绘(draw),流程图如下: Vie

  • 13问13答全面学习Android View绘制

    本文通过13问13答学习Android View绘制,供大家参考,具体内容如下 1.View的绘制流程分几步,从哪开始?哪个过程结束以后能看到view? 答:从ViewRoot的performTraversals开始,经过measure,layout,draw 三个流程.draw流程结束以后就可以在屏幕上看到view了. 2.view的测量宽高和实际宽高有区别吗? 答:基本上百分之99的情况下都是可以认为没有区别的.有两种情况,有区别.第一种 就是有的时候会因为某些原因 view会多次测量,那第

  • Android UI绘制流程及原理详解

    一.绘制流程源码路径 1.Activity加载ViewRootImpl ActivityThread.handleResumeActivity() --> WindowManagerImpl.addView(decorView, layoutParams) --> WindowManagerGlobal.addView() 2.ViewRootImpl启动View树的遍历 ViewRootImpl.setView(decorView, layoutParams, parentView) --&

  • 自己实现Android View布局流程

    相关阅读:尝试自己实现Android View Touch事件分发流程 Android View的布局以ViewRootImpl为起点,开启整个View树的布局过程,而布局过程本身分为测量(measure)和布局(layout)两个部分,以View树本身的层次结构递归布局,确定View在界面中的位置. 下面尝试通过最少的代码,自己实现这套机制,注意下面类均为自定义类,未使用Android 源码中的同名类. MeasureSpec 首先定义MeasureSpec,它是描述父布局对子布局约束的类,在

随机推荐