View工作原理 | layout

1,041 阅读4分钟

前言

当一个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。

整体流程如下图所示:

image.png

笔者水平有限,如有错误,欢迎评论、讨论。