系列文章传送门 (持续更新中..) :
自定义控件(一) Activity的构成(PhoneWindow、DecorView)
自定义控件(二) 从源码分析事件分发机制
自定义控件(三) 源码分析measure流程
在上一篇中我们详细分析了 View 工作三大流程中最复杂的 measure 流程, 掌握了 measure 流程后, layout 和 draw 流程就相对比较简单些了。
- 在布局流程中, 当 ViewGroup 的
layout
方法被父容器调用后它的位置将被确定下来, 然后它在onLayout
中遍历所有的子元素并调用它的layout
方法对子元素进行摆放, 而在 layout 中onLayout
方法又被调用, 如此反复直到布局完成。
简单讲:layout 方法确定 View 本身的位置, onLayout 确定所有子元素的位置
layout 过程 :
在看源码之前,先提出一个问题, View 的 getWidth()
和 getMeasuredWidth()
有什么区别?
public void layout(int l, int t, int r, int b) {
...
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame (l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
...
}
layout 方法中, 先调用 setFrame()
给自己的四个顶点赋值, 就确定了自己的位置。然后调用 onLayout
方法对子元素进行摆放
protected boolean setFrame(int left, int top, int right, int bottom) {
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}
看到没,mLeft、mTop 、mRight 、mBottom
就是这个 View 的四个顶点,当四个顶点的值呗确定,View 的位置就摆放完了。
由于在 View 中 onLayout()
方法是空实现, ViewGroup 的 onLayout()
是抽象方法, 所以就挑一个 ViewGroup 常用的子类 FrameLayout 看一下 (其它都类似, 自己可以去看下):
#FrameLayout - onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
layoutChildren(left, top, right, bottom, false);
}
onLayout
的参数直接传给 layoutChildren
,继续走:
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
...
很明显, 在 layoutChildren 遍历所有的子元素, 并调用其 layout 方法来摆放子元素,这样父容器在 layout 方法中完成自己的摆放后,通过 onLayout 方法去遍历调用子元素的 layout 方法,子元素又会通过 layout 方法确定自己的位置,这样一层一层传递下去从而完成整个 View 树的 layout 过程。
- 注意看调用 layout 传的参数,这里传入的
width
和height
, 其实就是这个 view 的测量宽/高。前面分析了 layout 中传入的参数会对 View 的四个顶点(mLeft、mTop 、mRight 、mBottom)
赋值来确定位置,这里我们来看一下getWidth()
的返回值:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
结合我们刚刚的源码分析不难看出来,mRight - mLeft 和 mBottom - mTop 的返回值不就分别是 view 的测量宽高么, 所以在系统 View 的默认实现中,以及开发中我们可以直接认为 getWidth() = getMeasuredWidth(),只是赋值时间不同, getHeight 和 getMeasuredHeight 同理,
getMeasuredWidth()
是在onMeasure()
方法中执行完成测量流程后并保存尺寸的时候被赋值,getWidth()
是在layout
方法中确定自己位置的时候被赋值。当然也存在两种情况会出现不相等:一种是某些极端情况系统需要多次执行measure流程,这时则除了最后一次measure,前几次的measure结果就可能存在不相等。另一种则是在 onLayout() 中调用 layout 时, 对传入的四个顶点值做了一些运算处理, 则这两个值也是不相等的,如下
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
...
child.layout(childLeft, childTop, childLeft + width + 100, childTop + height + 100);
...
}
// 或重写 layout 方法
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100, b + 100);
}
draw 过程 :
绘制过程就是将 View 绘制到屏幕上, 它分为4步:
- (1) 绘制背景(私有方法不能重写)
- (2) 主体绘制(一般重写此方法)
- (3) 绘制子元素
- (4) 绘制前景和滑动相关(绘制前景的支持是在 Android 6.0 之后)
具体从 draw
方法中可以明了的看出来:
public void draw(Canvas canvas) {
...
drawBackground(canvas);
...
onDraw(canvas);
...
dispatchDraw(canvas);
...
onDrawForeground(canvas);
...
}
绘制过程的传递是通过 dispatchDraw
来实现,dispatchDraw
中会遍历所有子元素的 draw
方法,如此反复下去直到绘制完成。
setWillNotDraw :
这是 View 的一个特殊方法,具体看源码:
/**
* 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);
}
从注释大致能看出来,如果一个 View 不需要对自己进行任何绘制,设置这个标志位为 true 后,系统会进行相应的优化,即绕过 draw()
方法,换而直接执行 dispatchDraw()
,以此来简化绘制流程。默认情况下 View 没有设置这个标志位, 而 ViewGroup 默认会启动这个标志位。
- 在实际开发中, 如果自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标志让系统进行绘制优化。 但是当明确知道一个 ViewGroup 需要在它的除
dispatchDraw()
以外的任何一个绘制方法内绘制内容,你显示的关闭这个 WILL_NOT_DRAW 这个标志位:View.setWillNotDraw(false)
;
觉得有用的话,点个再走呗~