前面学习了 View 三大流程中的 measure 过程,measure 过程确定了 View 的测量宽/高。这篇学习三大流程中的 layout 过程,它确定了 View 的最终宽/高和四个顶点的位置。我们知道,Android 中所有的控件组成可以看成一个 View 树状结构,总体分为两类:不包含子元素的普通 View 和包含子元素的 ViewGroup。当 ViewGroup 位置确定后,它会调用 onLayout() 方法,并在该方法中遍历其子元素并调用其 layout() 方法,接着继续在 layout() 方法中调用 onLaout() 方法,如此循环,知道将所有的 View 位置都确定完。所以综上:layout() 方法就是确定 View 本身的位置,而 onLayout() 方法则会确定所有子元素的位置。
我们先来分析 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 的四个顶点的位置。即初始化 mLeft、mRight、mTop 和 mBottom。
// 当 View 的四个顶点确定好了也就相当于 View 在父容器中的位置确定好了
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 接着调用 onLayout() 方法来确定该 View 中子元素的位置,循环往复,直到该 View 本身及其所有的子元素的
// 位置都确定完毕。
onLayout(changed, l, t, r, b);
...
}
...
}
与 onMeasure() 类似,由于不同的 ViewGroup 子类常作为父类容器使用,比如 LinearLayout、RelativeLayout 等等,它们有不同的布局特性,这导致它们的 layout 过程细节各不相同,所以 ViewGroup 无法做统一实现。下面我们分析一下 Android 中提供的继承了 ViewGroup 的 LinearLayout 中 onLayout() 的具体实现,以此来分析 ViewGroup 的 layout 过程。
@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);
}
}
接着,我们继续以 layoutVertical() 来分析:
void layoutVertical(int left, int top, int right, int bottom) {
...
// 遍历子元素
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
// 获取子元素测量(measure 过程)后的宽/高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
// 获取子元素的 LayoutParams
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
...
childTop += lp.topMargin;
// 为子元素指定对应的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
// 子元素的 Top 变大,这是因为父容器的布局方向为竖直的
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
可以看到,layoutVertical() 方法的关键就是 setChildFrame() 方法,接着看看它做了些什么:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
可以看到,setChildFrame() 方法仅仅是调用子元素的 layout() 方法,回到前面分析 layout() 方法我们知道,在 layout() 方法中又继续调用 onLayout() 方法,然后继续调用 layout() 方法,如此循环往复,直到该子元素以及该子元素内部的子元素的位置全部确定结束。这样一层一层地传递下去就完成了整个 View 树的 layout 过程。
分析完 layout 过程,我们再回到 layoutVertical() 方法中,可以看到 setChildFrame() 中的 width 和 height 就是子元素的测量宽/高。最开始分析过,view 四个顶点位置的确定方法为 setFrame(),如下:
protected boolean setFrame(int left, int top, int right, int bottom) {
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
所以,正常来说,我们在 layout() 过程完成后,通过 getWidth() 和 getHeight() 方法获取到的宽高和 measure 过程完成后通过 getMeasuredWidth() 和 getMeasuredHeight() 方法获取到的宽高一致才对啊?那为什么前面我们说,在极端情况下,View 的测量宽高和最终宽高有可能不一样呢?这里的极端情况其实就是我们人为的干预 layout 过程,比如重写 layout 方法:
@Override
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100, b + 100);
}
虽然这样做没有什么实际意义,但确实在结果上 layout 过程后的最终宽高比 measure 过程后的测量宽高都大 100px。综上:在 View 的默认实现中,View 的测量宽高(形成于 measure 过程)和最终宽高(形成于 layout 过程)大小一定是相等的,只是得到的时机不同而已。还有一种情况,由于 View 在某些情况下需要多次 measure 过程才能确定自己的测量宽高,但最终来说,View 的测量宽高和最终宽高大小还是一样的。