转载请标明出处: http://blog.csdn.net/airsaid/article/details/53872349
本文出自:周游的博客
前言
前面已经了解了 View 三大流程的 measure 和 layout 过程,这一篇继续学习最后的 draw 过程。draw 的过程依旧是在 ViewRootImpl#performTraversals 方法中调用的,其调用顺序是在最后, 相较与 measure 和 layout 过程要简单的多,它的作用就是将 View 绘制到屏幕上面。
View 的绘制
下面直接通过查看 View#draw 源码,来分析下其 draw 过程:
public void draw(Canvas canvas) {
......
/*
* 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
......
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
......
// Step 2, save the canvas' layers
......
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
......
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
......
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
......
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
......
canvas.drawRect(right - length, top, right, bottom, p);
}
......
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
上面的源码注释写的很清晰,通过查看后我们了解到 View 的绘制共分为如下六步:
- 1:绘制背景。
- 2:如果需要,保存图层信息。
- 3:绘制 View 的内容。
- 4:如果 View 有子 View,绘制 View 的子 View。
- 5:如果需要,绘制 View 的边缘(如阴影等)。
- 6:绘制 View 的装饰(如滚动条等)。
其中以上六步,第二步和第五步并不是必须的,所以我们只需重点分析其他四步即可。
绘制背景
绘制背景调用了 View#drawBackground 方法:
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);
}
}
绘制内容
绘制内容调用了 View#onDraw 方法,由于 View 的内容各不相同,所以该方法是一个空实现,需要由子类去实现:
protected void onDraw(Canvas canvas) {
}
绘制子 View
绘制子 View 调用了 View#dispatchDraw 方法,该方法同样是一个空实现:
protected void dispatchDraw(Canvas canvas) {
}
当只有包含子类的时候,才会去重写它,ViewGroup 不正好是吗? 来看下 ViewGroup 对该方法的实现吧:
protected void dispatchDraw(Canvas canvas) {
......
final int childrenCount = mChildrenCount;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
......
}
}
ViewGroup#dispatchDraw 方法的代码比较多,只分析重点,遍历了所有的子 View 并调用了 ViewGroup#drawChild 方法:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
该方法最终还是调用了子 View 的 draw 方法,似曾相识啊,和上篇中的 layout 过程是一样呢。
由于 ViewGroup 已经为我们实现了该方法,所以我们一般都不需要重写该方法。
绘制装饰
绘制装饰调用了 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);
}
}
该方法默认实现是绘制了滚动指示器、滚动条、和前景。
setWillNotDraw
View 中有一个特殊的方法,setWillNotDraw(boolean willNotDraw):
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
该方法是用于设置 WILL_NOT_DRAW 标记位的。默认情况下, View 没有启用这个优化标记位的,但是 ViewGroup 会默认启用。
如果当我们自定义的控件继承自 ViewGroup 并且本身并不具备任何绘制时,那么可以设置 setWillNotDraw 方法为 true,设置为 true 后,系统会进行相应的优化。
如果当我们知道我们自定义继承自 ViewGroup 的控件需要绘制内容时,那么需要设置 setWillNotDraw 方法为 false,来关闭 WILL_NOT_DRAW 这个标记位。
参考
- 《Android 开发艺术探索》