引言

前面我们讲到自定义View的测量和布局原理,并举例说明了这两个知识点的具体应用,本篇我们继续从源码入手看看View的绘制流程,与测量和布局流程,View的绘制过程要简单一些,主要流程如下:

View绘制流程图.png

(一)View的draw流程源码分析:

/**

* 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)。

* 绘制过程:

* 1. 绘制view背景

* 2. 绘制view内容

* 3. 绘制子View

* 4. 绘制装饰(渐变框,滑动条等等)

* 注:

* a. 在调用该方法之前必须要完成 measure和layout 过程

* b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)

* c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法

*/
public void draw(Canvas canvas) {
...
// 步骤1: 绘制本身View背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 若有必要,则保存图层(还有一个复原图层)
// 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过,因此在绘制时,节省 layer 可以提高绘制效率
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 步骤2:绘制本身View内容
if (!dirtyOpaque)
onDraw(canvas);
// View 中:默认为空实现,需复写
// 步骤3:绘制子View
// 由于单一View无子View,故View 中:默认为空实现
// ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
dispatchDraw(canvas);
// 步骤4:绘制装饰,如滑动条、前景色等等
onDrawScrollBars(canvas);
return;
}
...
}

1.绘制背景:

private void drawBackground(Canvas canvas) {
// 拿到背景 drawable
final Drawable background = mBackground;
if (background == null) {
return;
}
// 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
setBackgroundBounds();
.....
// 获取 mScrollX 和 mScrollY值,即偏移量
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {//按位或运算相当于与运算
background.draw(canvas);
} else {
// 若 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移
canvas.translate(scrollX, scrollY);
// 调用 Drawable 的 draw 方法绘制背景
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}

2.onDraw()方法是空方法,需要在子类中覆写绘制自己的内容。

由于View没有子View,所以dispatchDraw是空实现.

4.绘制装饰器,如前置景、滚动条等。

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);
}
}

(二)ViewGroup的draw流程分析。

ViewGroup的绘制流程与View绘制流程基本一致,不同的是,它覆写了dispatchDraw()方法:

protected void dispatchDraw(Canvas canvas) {
......
// 1. 遍历子View
final int childrenCount = mChildrenCount;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 2. 绘制子View视图
more |= drawChild(canvas, transientChild, drawingTime);
}
....
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
// 调用子 View 的 draw()进行子View的绘制,如果是ViewGroup,就继续调用dispatchDraw()方法,这样就实现一颗View树的绘制。
return child.draw(canvas, this, drawingTime);
}

(三)细节补充:View的重绘制简单说明。

1.View有两个很重要的方法:invalidate和requestLayout,常用于View重绘和更新;

invalidate()方法会执行onDraw过程,重绘View树,注意它仅仅调用绘制流程,不影响测量和布局;

/**
*invalidate方法会执行onDraw过程,重绘View树
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* 
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}

3.requestLayout()方法:当View的边界,也可以理解为View的宽高,发生了变化,可以调用requestLayout方法重新对View布局;

4.View执行requestLayout方法,会向上递归到顶级父View中,再执行这个顶级父View的requestLayout,所以其他View的onMeasure,onLayout也可能会被调用。

调用invalidate方法只会执行onDraw方法;调用requestLayout方法只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。所以当我们进行View更新时,若仅View的显示内容发生改变且新显示内容不影响View的大小、位置,则只需调用invalidate方法;若View宽高、位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法。