View的绘制-layout流程详解

栏目: IOS · Android · 发布时间: 5年前

内容简介:根据 measure 测量出来的宽高,确定所有 View 的位置。View 本身的位置是通过它的四个点来控制的:以下涉及到源码的部分都是版本27的,为方便理解观看,代码有所删减。
View的绘制-layout流程详解

作用

根据 measure 测量出来的宽高,确定所有 View 的位置。

具体分析

View 本身的位置是通过它的四个点来控制的:

View的绘制-layout流程详解

以下涉及到源码的部分都是版本27的,为方便理解观看,代码有所删减。

layout 的流程

先通过 measure 测量出 ViewGroup 宽高,ViewGroup 再通过 layout 方法根据自身宽高来确定自身位置。当 ViewGroup 的位置被确定后,就开始在 onLayout 方法中调用子元素的 layout 方法确定子元素的位置。子元素如果是 ViewGroup 的子类,又开始执行 onLayout,如此循环往复,直到所有子元素的位置都被确定,整个 View 树的 layout 过程就执行完了。

在上一节 《View的绘制-measure流程详解》 中说过,View 的绘制流程是从 ViewRootViewImpl 中的 performMeasure()performLayoutperformDraw 开始的。在执行完 performMeasure() 后,开始执行 performLayout 方法:(以下源码有所删减)

//ViewRootViewImpl 类
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
    int desiredWindowHeight) {
    final View host = mView;
    /*代码省略*/
    //开始执行 View 的 layout 方法
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    /*代码省略*/
}
复制代码

如此,就执行了 View 的 layout 方法:

//View 类
public void layout(int l, int t, int r, int b) {
    /*代码省略*/
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    //确定 View 的四个点后,调用 setOpticalFrame/setFrame 方法来控制 View 位置。
    //方法 1--->setOpticalFrame
    //方法 2--->setFrame
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //执行 onLayout 方法
        onLayout(changed, l, t, r, b);
        /*代码省略*/
    }
    /*代码省略*/
}

//方法 1---->setOpticalFrame 
//View 类
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
    Insets parentInsets = mParent instanceof View ?
            ((View) mParent).getOpticalInsets() : Insets.NONE;
    Insets childInsets = getOpticalInsets();
    //内部最终也是调用 setFrame  方法
    return setFrame(
            left   + parentInsets.left - childInsets.left,
            top    + parentInsets.top  - childInsets.top,
            right  + parentInsets.left + childInsets.right,
            bottom + parentInsets.top  + childInsets.bottom);
}

//方法 2--->setFrame
//View 类
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        /*代码省略*/
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        //控件的大小和位置有没有改变
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // Invalidate our old position
        invalidate(sizeChanged);
        //对 View 的四个点赋值
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        mPrivateFlags |= PFLAG_HAS_BOUNDS;


        if (sizeChanged) {
            //这里 sizeChange 方法内部调用了 onSizeChanged 方法。
            //所以当控件的大小和位置改变的时候会回调 onSizeChanged 方法
            //方法 3--->sizeChange
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
        /*代码省略*/
    }
    return changed;
}

//方法 3--->sizeChange
// View 类
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
    //执行 onSizeChanged 方法
    onSizeChanged (newWidth, newHeight, oldWidth, oldHeight);
    if (mOverlay != null) {
        mOverlay.getOverlayView().setRight(newWidth);
        mOverlay.getOverlayView().setBottom(newHeight);
    }
    rebuildOutline();
}
复制代码

接下来我们再看 onLayout 方法,在 View 中找到 onLayout 方法,会发现这是一个空实现的方法,里面什么也没有执行,那么我们就在 ViewGroup 中找 onLayout 的实现,发现只是一个抽象方法。

//View 类
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
复制代码
//ViewGroup 类
@Override
protected abstract void onLayout(boolean changed,
        int l, int t, int r, int b);
复制代码

在 View 类中 onLayout 是一个空实现不难理解,因为如果一个控件继承了 View ,它是没有子元素的,不需要确定子元素的位置,只需要确定自己的位置就够了。

在 ViewGroup 中是一个抽象方法,意思也很明显了,在控件继承自 ViewGroup 的时候,我们必须重写 onLayout 方法。因为如 LinearLayoutRelativeLayout ,他们的布局特性都是不一样的,需要各自根据自己的特性来进行制定确定子元素位置的规则。

下面以 LinearLayout 为例,分析 onLayout 里面的逻辑。

LinearLayout 的 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);
    }
}
复制代码

layoutVerticallayoutHorizontal 执行流程类似,就分析 layoutVertical

//LinearLayout 类
void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;

    // Where right end of child should go
    final int width = right - left;
    int childRight = width - mPaddingRight;

    // Space available for child
    int childSpace = width - paddingLeft - mPaddingRight;

    final int count = getVirtualChildCount();
    /*省略代码*/

    for (int i = 0; i < count; i++) {
        //遍历子 View
        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();

            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }
            //加上 子元素的 topMargin 属性值
            childTop += lp.topMargin;
            //设置子元素的 位置
            //方法 1 ----->setChildFrame
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            /*加上子元素的高度、bottomMargin、偏移量,就是下一个子元素的 初始 top。
            如此,子元素就从上到下排列,符合我们所知的 LinearLayout 的特性*/
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

//方法 1 ----->setChildFrame
//LinearLayout 类
private void setChildFrame(View child, int left, int top, int width, int height) {
    //最终又调用了子元素的 layout 方法.
    child.layout(left, top, left + width, top + height);
}
复制代码

可以看到,在遍历子元素后,又调用了子元素的 layout 方法。子元素如果是继承 ViewGroup,还是会调用到子元素的 onLayout 方法,遍历自己的子元素,调用自己子元素的 layout 方法,如此循环递归,就完成了整个 View 树的 layout 流程。

getWidth、getMeasureWidth分析

getWidth 获取的值和 getMeasureWidth 获取的值有什么不同吗? 首先看 getWidth 源码:

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

然后再看 mRight 和 mLeft 赋值(在我们分析 layout 的时就有展现):

以下为超精简代码,哈哈:

//View 类
public void layout(int l, int t, int r, int b) {
    //setOpticalFrame 最终也是调用了 setFrame 方法
     boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
}

//View 类
protected boolean setFrame(int left, int top, int right, int bottom) {
    //对四个值进行赋值
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
}
复制代码

然后我们再回顾调用 layout 的方法:

//LinearLayout 类
void layoutVertical(int left, int top, int right, int bottom) {
    for (int i = 0; i < count; i++) {
        final int childWidth = child.getMeasuredWidth();
        final int childHeight = child.getMeasuredHeight();
        //将获取的 childWidth 和 childHeight 传入进去
        setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
    }
}
//LinearLayout 类
private void setChildFrame(View child, int left, int top, int width, int height) {
    //子元素 layout 的方法中传入的 right  = left + width
    //子元素 layout 的方法中传入的 bottom  = left + height
    child.layout(left, top, left + width, top + height);
}
复制代码

这下就一目了然了,在 getWidth() 方法中 mRight - mLeft 其实就是等于 childWidth ,也就是 getWidth() = getMeasureWidth()

那么,他们两个有不相等的时候吗?有的:

//自定义 View 
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    //手动改变传入的值 
    super.onLayout(changed, left, top, right+100, bottom+100);
}
复制代码

如果我们在自定义 View 中重写 onLayout 方法,并手动改变传入的值, getWidth()getMeasureWidth() 的值自然就不一样了。不过这样做好像没什么意义?

还有一种情况就是在某些情况下,View 需要多次 measure 才能确定自己的测量宽/高,在前几次测量的时候,其得出的测量宽/高 ( getMeasureWidth()/ getMeasureHeight()) 和最终宽/高 ( getWidth()/ getHeight()) 可能不一致,但是最终他们的值还是相等的。(这段话摘自刚哥的《Android 开发艺术探索》)

getHeight 和 getMeasuredHeight 过程类似,这里就不分析了。

所以我们在日常开发中,我们可以认为两者就是相等的。

另:可能还是会有疑惑,这里只分析了 LinearLayout,那么其他布局也适用这个结论么?

FrameLayout 类

//FrameLayout 类
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();
            //从这里看出 FrameLayout 也是适用这个结论的
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}
复制代码

RelativeLayout 类

//RelativeLayout 类
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int count = getChildCount();

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            RelativeLayout.LayoutParams st =
                    (RelativeLayout.LayoutParams) child.getLayoutParams();
            //Relativelayout 这里传入的是 LayoutParams 中的属性
            //st.mRight 和 st.mBottom 赋值很复杂,不过他们也是适用这个结论的,具体可以查看源码分析
            child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
        }
    }
}
复制代码

其他我们日常开发使用的布局也是适用于这个结论的。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

设计方法卡牌

设计方法卡牌

罗莎 等 / 电子工业出版社 / 2017-7 / 79.00

本套设计素材提供了54种设计方法,以卡牌的形式展示给读者,每张卡牌包括该设计方法的基本描述、目的、时间成本、工具渠道、使用阶段、操作步骤及其归类属性等信息。在做设计时,可以根据自己的需求进行卡牌的选择和组合,让设计工作更有灵活性和趣味性。同时,依据设计产品属性的不同,卡牌提供了选择设计方法的推荐模板、方法组合模板、产品阶段划分模板,给初学者在做设计时提供一定的理论依据,帮助读者启发设计灵感,剖析设......一起来看看 《设计方法卡牌》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码