View绘制就好比画画,抛开Android概念,如果要画一张图,首先会想到哪几个基本问题:
- 画多大?
- 画在哪?
- 怎么画?
Android绘制系统也是按照这个思路对View进行绘制,上面这些问题的答案分别藏在:
- 测量(measure)
- 定位(layout)
- 绘制(draw)
这一篇将从源码的角度分析“绘制(draw)”。View
绘制系统中的draw
其实是讲的是绘制的顺序,至于具体画什么东西是各个子View
自己决定的。
这是Android自定义控件系列文章的第三篇,系列文章目录如下:
View.draw()
在分析View
的测量和定位时,发现它们都是自顶向下进行地,即总是由父控件来触发子控件的测量或定位。不知道“绘制”是不是也是这样?,以View.draw()
为切入点,一探究竟:
public void draw(Canvas canvas) {
// Step 1, draw the background, if needed
//第一步:绘制背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
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);
drawAutofilledHighlight(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);
// Step 7, draw the default focus highlight
//第七步:绘制默认高亮
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we’re done...
return;
}
}
这个方法实在太长了。。。还好有注释帮我们提炼了一条主线。注释说绘制一共有6个步骤,他们分别是:
- 绘制控件背景
- 保存画布层
- 绘制控件自身内容
- 绘制子控件
- 绘制褪色效果并恢复画布层(感觉这一步和第二步是对称的)
- 绘制装饰物
为啥提炼了主线后还是觉得好复杂。。。还好注释又帮我们省去了一些步骤,注释说“通常情况下第二步和第五步会跳过。”在剩下的步骤中有三个步骤最最重要:
- 绘制控件背景
- 绘制控件自身内容
- 绘制子控件
读到这里可以得出结论:View
绘制顺序是先画背景(drawBackground()
),再画自己(onDraw()
),接着画孩子(dispatchDraw()
)。晚画的东西会盖在上面。
先看下drawBackground()
:
private void drawBackground(Canvas canvas) {
//Drawable类型的背景图
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
...
//绘制Drawable
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
背景是一张Drawable
类型的图片,直接调用Drawable.draw()
将其绘制在画布上。接着看下onDraw()
:
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
protected void onDraw(Canvas canvas) {
}
}
View.onDraw()
是一个空实现。想想也对,View
是一个基类,它只负责抽象出绘制的顺序,具体绘制什么由子类来决定,看一下ImageView.onDraw()
:
public class ImageView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
//绘制drawable
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
final int saveCount = canvas.getSaveCount();
canvas.save();
if (mCropToPadding) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
scrollX + mRight - mLeft - mPaddingRight,
scrollY + mBottom - mTop - mPaddingBottom);
}
canvas.translate(mPaddingLeft, mPaddingTop);
if (mDrawMatrix != null) {
canvas.concat(mDrawMatrix);
}
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
}
ImageView
的绘制方法和View
绘制背景一样,都是直接绘制Drawable
。
ViewGroup.dispatchDraw()
View.dispatchDraw()
也是一个空实现,想想也对,View
是叶子结点,它没有孩子:
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {
}
}
所以ViewGroup
实现了dispatchDraw()
:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
protected void dispatchDraw(Canvas canvas) {
...
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
//当没有硬件加速时,使用预定义的绘制列表(根据z-order值升序排列所有子控件)
final ArrayList<View> preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
//自定义绘制顺序
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
//遍历所有子控件
for (int i = 0; i < childrenCount; i++) {
...
//如果没有自定义绘制顺序和预定义绘制列表,则按照索引i递增顺序遍历子控件
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
//触发子控件自己绘制自己
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
final int childIndex;
if (customOrder) {
final int childIndex1 = getChildDrawingOrder(childrenCount, i);
if (childIndex1 >= childrenCount) {
throw new IndexOutOfBoundsException("getChildDrawingOrder() "
+ "returned invalid index " + childIndex1
+ " (child count is " + childrenCount + ")");
}
childIndex = childIndex1;
} else {
//1.如果没有自定义绘制顺序,遍历顺序和i递增顺序一样
childIndex = i;
}
return childIndex;
}
private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
int childIndex) {
final View child;
if (preorderedList != null) {
child = preorderedList.get(childIndex);
if (child == null) {
throw new RuntimeException("Invalid preorderedList contained null child at index "
+ childIndex);
}
} else {
//2.如果没有预定义绘制列表,则按i递增顺序遍历子控件
child = children[childIndex];
}
return child;
}
}
结合注释相信你一定看懂了:父控件会在dispatchDraw()
中遍历所有子控件并触发其绘制自己。 而且还可以通过某种手段来自定义子控件的绘制顺序(对于本篇主题来说,这不重要)。
沿着调用链继续往下:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
// 绘制ViewGroup的一个孩子
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
}
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
//绘制
draw(canvas);
}
...
}
ViewGroup.drawChild()
最终会调用View.draw()
。所以,View
的绘制是自顶向下递归的过程,“递”表示父控件在ViewGroup.dispatchDraw()
中遍历子控件并调用View.draw()
触发其绘制自己,“归”表示所有子控件完成绘制后父控件继续后序绘制步骤`
总结
经过三篇文章的分析,对View
绘制流程有了一个大概的了解:
View
绘制流程就好比画画,它按先后顺序解决了三个问题 :- 画多大?(测量measure)
- 画在哪?(定位layout)
- 怎么画?(绘制draw)
- 测量、定位、绘制都是从
View
树的根结点开始自顶向下进行地,即都是由父控件驱动子控件进行地。父控件的测量在子控件件测量之后,但父控件的定位和绘制都在子控件之前。 - 父控件测量过程中
ViewGroup.onMeasure()
,会遍历所有子控件并驱动它们测量自己View.measure()
。父控件还会将父控件的布局要求与子控件的布局诉求相结合形成一个MeasureSpec
对象传递给子控件以指导其测量自己。View.setMeasuredDimension()
是测量过程的终点,它表示View
大小有了确定值。 - 父控件在完成自己定位之后,会调用
ViewGroup.onLayout()
遍历所有子控件并驱动它们定位自己View.layout()
。子控件总是相对于父控件左上角定位。View.setFrame()
是定位过程的终点,它表示视图矩形区域以及相对于父控件的位置已经确定。 - 控件按照绘制背景,绘制自身,绘制孩子的顺序进行。父控件在完成绘制自身之后,会调用
ViewGroup.dispatchDraw()
遍历所有子控件并驱动他们绘制自己View.draw()