前言
当一个View树的measure执行完后,所有View的测量大小就确定了,而layout即布局的作用是ViewGroup用来确定子元素的位置。
注意这里的描述,是ViewGroup用于确定子元素位置,这是因为View的layout方法就可以确定View本身的位置,而onLayout方法则会去确定所有子元素的位置。
这个过程比measure要稍微简单,但是和measure却不一样,measure过程对于ViewGroup来说是需要所有子View都measure完,最后才可以确定ViewGroup的大小;但是layout对于ViewGroup来说,调用该ViewGoup的layout方法时,其位置就确定了,只需要遍历去确定子元素位置即可。
正文
所以layout过程也比较简单,我们还是从ViewRootImpl来看,是如何布局整个View树的。
ViewRootImpl开始
在ViewRootImap中的performLayout方法中,有如下代码:
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
这里的host就是DecorView,而这里我们可以发现调用layout的参数就是View的测量宽高,即把DecorView的测量宽高传递到layout方法中,而在上一篇文章我们说过,顶级View的宽高一般情况下就是屏幕的宽高。
View的layout
这里我们直接看一下View的layout函数:
public void layout(int l, int t, int r, int b) {
...
//这里是为了判断是否是重新布局
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//关键代码是setFrame方法,设定View的4个顶点位置
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);
...
}
setFrame
这里有个关键方法是setFrame,View也就是通过这个方法来设置View的位置,setFrame方法如下:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
该方法就是设置View在父View中的位置,其中mLeft、mTop、mRright和mBottom就是当前View相当于父布局的左、上、右、下的坐标,这4个属性非常重要,因为View的getWidth方法定义如下:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
可以发现其值就是俩者的差值。
测量大小和View大小
所以这里就很清晰地看出一个问题,就是测量大小和View的大小默认是一样的,因为调用layout方法的参数就是测量的大小。
但是和measure方法不一样,layout方法是一个非final方法,所以我们重写layout方法,来改变系统的赋值,如下:
override fun layout(l: Int, t: Int, r: Int, b: Int) {
super.layout(l, t, r+100, b+100)
}
上述代码就会导致View的大小比测量大小在宽高都大100px。
我们继续分析上面的layout方法,它会先设置自己的位置,然后接着会回调onLayout方法,在该方法中我们可以对子View进行布局。
onLayout
该方法在View和ViewGoup中都没有具体实现,如下:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
这是因为和ViewGoup的measure一样,在该方法内需要对子View进行布局,而不同的ViewGroup有着不同的布局策略。
为了说明,我们就以LinearLayout线性布局举例。
LinearLayout的onLayout
下面代码是线性布局的onLayout方法:
//LinearLayout的布局代码
@Override
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);
}
}
//竖向排列,这里的4个参数就是LinearLayout本身的位置
void layoutVertical(int left, int top, int right, int bottom) {
...
//遍历所有View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//获取子View的测量宽高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//结合LinearLayout的LayoutParams计算
...
//得到该View的4个顶点数据
childTop += lp.topMargin;
//调用子View的方法
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
通过上面的方法,我们就可以按照自己的需求设定子View的位置,看一下这个setChildFrame方法:
//就是调用子View的layout方法
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
果不其然,这里又是调用子View的layout方法,即把布局事件传递给了子View。
总结
首先View的layout过程是在measure之后的,因为它需要View的测量宽高,来默认作为View的大小。其次和measure不一样的是在layout过程中,是先调用setFrame方法来确定自己的位置,然后在onLayout方法再去布局子View。
整体流程如下图所示:
笔者水平有限,如有错误,欢迎评论、讨论。