android view draw,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的绘制流程也讲述完毕了,希望这篇文章对你们起到帮助作用,谢谢你们的阅读。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值