注:本文使用 sdk 23 作为源码参考。
前言
关于 View 的绘制流程,网上铺天盖地的文章已经都把这个机制说烂了,笔者撰写此文一面为了方面自己后期回顾,一面也试着使用更通俗一点的方式来阐述这个机制。
ViewRootImpl#performTraversals()
众所周知, ViewRootImpl#performTraversals()
是触发 View 绘制流程的起始点,而在 ViewRootImpl#performTraversals()
中会触发相应的 ViewRootImpl#performMeasure()
、ViewRootImpl#performLayout()
、ViewRootImpl#performDraw()
对应着测量
、布局
、绘制
三个过程。(不得不说函数、变量的命名对源码的理解还是有很大的帮助的,从 ViewRootImpl#performTraversal()
这个函数名就应该能猜到这个函数是完成遍历的过程,完成什么的遍历?完成测量
的遍历、布局
的遍历、绘制
的遍历),为了加深各位读者的理解,笔者将在源码解析的过程中,一步一步绘制流程图,比起文章尾部丢上一张完整的流程图,笔者认为这样的做法会更加友善——
ViewRootImpl#performMeasure()
首先是测量过程,删除无关代码后简化代码如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到,实际上在 ViewRootImpl#performMeasure()
中调用了 View 的 measure()
方法,打开 View#measure()
方法看一看,可以看到这个方法是 fianl
的,所以对于它的子类来说是不能够覆写该方法的,而在其内部调用了 View#onMeasure()
方法,那么接下来就该看看 onMeasure()
方法的实现了,在 View 中 onMeasure()
方法实现如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension(measuredWidth, measuredHeight)
方法用官方文档的意思就是用来保存测量后的宽高。
那么 ViewGroup 中的实现呢?实际上在源码中可以看到,ViewGroup 并没有按照常理将其设置为 abstract 类型,但是 onMeasure()
上方的注释文档提到:子类需要重写它才能够获取到精确、有效的测量值
,所以对于 View 的子类来说,都应该去重写该方法,所以实际上不仅仅是针对于 ViewGroup 来说,对于 TextView、ImageView 来说也是需要重写 onMeasure()
方法的。对于 View 的 onMeasure()
方法本文就不加以扩展了,毕竟测量
这种操作对于纯粹的 View 来说就是测量自己的大小,所以不妨着重看看 ViewGroup 的实现,以 LinearLayout 举例:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
LinearLayout 根据 orientation 的设置来选择测量方式,笔者这里选择 LinearLayout#measureVertical()
来阐述,LinearLayout#measureHorizontal()
意义等同。LinearLayout#measureVertical()
源码删减后如下:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// ...
} else {
// ...
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
}
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
for 循环是对子 View 的遍历。if 判断语句的执行条件中有一点是 lp 的高度是0,也就意味着子 View 的 layout_height 设置为了 0,这种情况就不需要对子 View 进行 measure 操作了,因为对于竖直 LinearLayout 来说,它更关注于子 View 的高度。这也就是意味着正常情况下都是会走 else 分支,也就是说正常情况下每个 view 都会被执行 LinearLayout#measureChildBeforeLayout()
方法,跟踪 LinearLayout#measureChildBeforeLayout()
方法可以发现实际上底层是调用了 View 的 measure()
方法——
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// ...
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
所以实际上对于 ViewGroup 来说,它的 onMeasure()
实际上就是调用各个子 View 的 measure()
方法来将它们的某些值做一些汇总然后拼凑成自己的宽高。
实际上上述源码中的
measureChildWithMargins()
只是 ViewGroup 提供的一种测量子 View 的函数,与此类似的还有ViewGroup#measureChild()
。除了测量单个子 View 的函数外,ViewGroup 还有提供measureChildren()
这种子 View 遍历测量的函数——
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
但是实际上该函数在 ViewGroup 子类实现中运用的不多,像 LinearLayout/FrameLayout/RelativeLayout 中都是自行实现子 View 遍历,底层只会调用
measureChild()
或者measureChildWithMargins()
。注:笔者见部分书籍和博客常谈到
measureChildren()
这个函数,且甚至有博客笼统地称 ViewGroup 均会调用该函数来遍历测量子 View,故特此撰写 tip。且笔者认为从使用率上来说,该函数在 ViewGroup 中运用地太少,意义并不是很大。
ViewRootImpl#performLayout()
ViewRootImpl#performLayout()
其实与 ViewRootImpl#performMeasure()
类似,底层实现通过调用 View#layout()
来实现的——
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
// ...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// ...
}
所以不妨打开 layout()
方法看一看——
public void layout(int l, int t, int r, int b) {
setFrame(l, t, r, b);
onLayout(changed, l, t, r, b);
}
可以看到,实际上 View#layout()
做了两步,一步是调用 setFrame()
设置自身上下左右四个顶点的位置,这样自身的位置就已经布好了,第二步是 onLayout()
——
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
View 中 onLayout()
方法竟然是一个空实现,不仅如此,ViewGroup 作为抽象类,onLayout()
是它唯一声明的抽象方法,可见该方法对于 ViewGroup 来说有多重要了,它的官方注释说到:当 View 需要给它的孩子设置大小和位置的时候应该被调用
。所以从这里可以看出,对于纯粹的 View 来说,onLayout()
的意义可能不是很大(从源码看来,View 中只有 TextView 对其稍有扩展),但是对于 ViewGroup 来说确是至关重要的一个函数。毕竟作为一个 ViewGroup 来说,多样性的体现就在于对子 View 的摆放。
同样的,拿 LinearLayout 来举例,看看 LinearLayout 的 onLayout()
源码实现——
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
同样的,我们不妨选择参考 layoutVertical()
的实现——
void layoutVertical(int left, int top, int right, int bottom) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
}
}
内部会对子 View 进行遍历,并调用 setChildFrame()
,而 setChildFrame()
的底层实现也就是 View#layout()
——
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
所以,对于 ViewGroup 来说,它的 layout()
方法先会对自己进行定位(setFrame()
),再遍历调用子 View 的 layout()
(/setFrame()
) 将所有的子 View 进行布局。
ViewRootImpl#performDraw()
等同于 performMeasure()
、performLayout()
,ViewRootImpl#performDraw()
底层会调用 draw()
方法——
private void performDraw() {
// ...
draw();
// ...
}
当然,这里可以注意到这个 draw()
方法是位于 ViewRootImpl 中而不是 View 中的,打开 draw()
并删除无关代码:
private void draw(boolean fullRedrawNeeded) {
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
继续打开 drawSoftwar()
方法:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
mView.draw(canvas);
}
所以最终 ViewRootImpl#draw()
最终底层还是通过 View#draw()
来实现的。
来看看 View 的 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
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;
}
}
源码注释已经说得很清楚了:
1.绘制背景
2.如果有需要的话,保存图层,为渐变做准备
3.绘制自身
4.绘制子 View
5.如果有需要的话,绘制渐变并重新存储图层
6.绘制装饰
从上方的源码也可以看出,大部分2、5点是可以跳过的,所以日常开发中需要注意到1、3、4、6的执行顺序就好了。而从这里可以看出两点,其一对于 ViewGroup 来说,要关注到第4点也就是 dispatchDraw()
的实现了,实际上对于 ViewGroup 来说,它的实现也等同于测量和布局,也就是循环遍历调用子类的 draw()
方法;其二对于 View 来说,需要关注到第3点也就是 onDraw()
的实现了,实际上在日常开发自定义 View 中 onDraw()
方法应该是最常见覆写的 API 了,所以笔者再此也不做扩展了。
some tips
onDraw()
在本文中并未涉及,笔者照搬《Android 开发艺术探索》上的内容了——如果是继承自 View,需要自行支持 wrap_content,且 padding 也需要自行处理。
这里需要加粗的地方就是继承自 View,如果是 TextView 等原生控件可以不考虑处理 wrap_content 和 padding,因为原生控件已经覆写了 onDraw()
方法。
dispatchDraw()
具有一定的经验的读者知道,对于 ViewGroup 来说,大部分情况下 draw(Canvas canvas)
方法是不会被调用,但是 dispatchDraw()
方法在正常情况下都是会被调用的。缘由在 View 中的 updateDisplayListIfDirty()
中有这么一段:
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
可以看到会对一些 flag 进行判断,如果判断结果符合则执行 dispatchDraw()
方法,否则才执行 draw()
方法。那么这个 flag 如何设置呢?通过 View#setWillNotDraw(boolean)
就可以设置了,对于 View 来说是关闭这个 flag 的,而对于 ViewGroup 来说是默认开启这个 flag 的,当开发者可以手动开启或者调用部分绘制 API(如 drawBackground()
)的时候才会关闭这个 flag。那么这样做的目的是什么呢——对于 ViewGroup 来说,它存在的意义主要在于测量和布局的过程,而视图『具体效果』的展示其实都在子 View 身上,ViewGroup 的重点并不在此。
各位读者可以结合实际情况想想是不是这么一回事,理解了这个概念就不再会困惑于为什么 ViewGroup 的 draw()/onDraw()
方法不一定会被调用了。所以对于自定义 ViewGroup 来说,在有相应的需求下尽量去覆写 dispatchDraw()
而不是 onDraw()
方法,但是事实上针对一般的 ViewGroup 来说也不需要去覆写该方法,沿用 ViewGruop 的即可(源码中 LinearLayout、FrameLayout、RelativeLayout 等常见布局沿用 ViewGroup 的 dispatchDraw()
方法)。