Android View的工作原理3 — layout流程

前言

本篇文章来说测量、布局和绘制的三大步骤中的第二步:布局,简单来说经过前面介绍的测量流程后,每个View它的大小就基本确定了,但是每个View需要如何放置,这就是布局的作用了。

正文

由于布局的过程比测量要简单一些,这里就直接看源码分析一遍流程。

ViewRootImpl开始

和前面文章里介绍的测量流程一样,在一个页面你一旦调用requestLayout()方法后,就会执行ViewRootImpl中的requestLayout()方法,然后执行View的绘制。下面代码简单介绍一下流程,可以不用细看:

//ViewRootImpl中的requestLayout方法
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        //执行遍历
        scheduleTraversals();
    }
}
复制代码
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //利用handler,发送绘制Runnable
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
复制代码
//真正的绘制方法
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
复制代码
void doTraversal() {
    if (mTraversalScheduled) {
        ...
        //执行遍历
        performTraversals();
        ...
    }
}
复制代码
private void performTraversals() {
    ...
    //这里的host就是这个View链的根View
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
 }
复制代码

到这里就把布局这个操作给传递到了View上。

layout过程

Layout的作用就是ViewGroup来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中遍历所有子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。

比如上面的流程从ViewRootImapl到View的布局事件调用,那个host一般就是mDecorView,即是一个FrameLayout,所以mDecorView的位置是确定的。

确定mDecorView的方法是调用layout方法,所以我们先分析一下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);
        ...
}
复制代码

这里主要可以确定2点:

  • View是通过setFrame方法来真正确定4个顶点的位置。

  • 在layout方法内会调用onLayout方法来让用户自定义ViewGroup子View的布局方式。

所以当mDecorView是FrameLayout时,就会调用其对应的FrameLayout的onLayout方法。

ViewGroup的onLayout方法

View和ViewGroup都没有真正实现onLayout方法,这个需要ViewGroup去实现,我们就看一个简单的例子,看一下线性布局的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) {
    final int paddingLeft = mPaddingLeft;
    int childTop;
    int childLeft;
    //计算具体的padding
    ...
    //遍历所有View
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            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);
}
复制代码

果不其然,这里又是调用其layout方法。

getWidth和getMeasureWidth的区别

这是一个很经典的问题,这2者的区别,其实看了布局的源码就非常容易理解了。

其中getWidth的方法是:

public final int getWidth() {
    return mRight - mLeft;
}
复制代码

而getMeasureWidth方法是:


public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}
复制代码

后者是测量宽高,我们很好理解,而前者其实正常情况下也就是测量宽高,看源码可知传入给layout函数的几个参数就是测量宽高。

但是也没有让这俩者不一致呢,是有办法的,但是不建议,也就是重写View的layout方法:

public void layout(int l,int t,int r,int b){
    super.layout(l,t,r + 100,b)
}
复制代码

这里就会导致原来代码传入的测量宽高会不一样,但是一般不会这么写。

总结

View的布局还是蛮简单的,一句话总结一下就是调用layout方法传入的参数用来确定自己的位置,setFrame方法是真正确定View的4个顶点,然后重新onLayout方法来确定子View的位置。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享