UI的测量、布局和绘制源码详解

2022-02-05  本文已影响0人  天上飘的是浮云

因为之前撸了AMS、PMS。前几天也跟进WMS看了WMS-01WMS-02,知道了ViewRootImpl会管理所有View的绘制策略,都是由他控制。还有Choreographer编舞者类会去底层申请Vsync垂直同步信号,获取回来后会去调用ViewRootImpl的performTraversals方法,这个方法中会有performMeasure测量方法、performLayout布局方法和performDraw绘制方法。

一、确定下我们要搞清的东西

二、ViewRootImpl为啥管理所有View的绘制流程。

2.1 首先ViewRootImpl是由WindowManagerGlobal实例化的,而WindowManagerGlobal是单例,
2.2 ViewRootImpl的performMeasure、performLayout和performDraw最终都是调用View的measure、layout和draw方法。
2.3 我们再看看View中的requestLayout、invalidate和postInvalidate最终都是调用了谁?

三、View的MeasureSpec创建规则?

看图说话
3.1 当父容器为EXACTLY模式时:
3.2 当父容器为AT_MOST模式时:
3.3 当父容器为UNSPECIFIED模式时:

四、MeasureSpec的含义以及它的结构是怎样的?

4.1 为什么MeasureSpec的mode和size要合到一起?

我们知道MeasureSpec有三种模式:EXACTLY、AT_MOST和UNSPECIFIED。那么他们其实用两个二进制位就可以表示了:如EXACTLY用01、AT_MOST用10、UNSPECIFIED用00表示。

我们知道一个int有4个字节,32比特位。Google工程师为了节省内存,将前两位用于Mode的表示、后30位用于Size表示。因为Mode总共就三种模式,所以两位足以。而size范围为0~1073741823(1 << MeasureSpec.MODE_SHIFT) - 1)也足够了。

将两者合二为一就可以用一个int值表示两种数据了,不然的话还得用两个int值表示,一个表示mode,一个表示Size。这样的设计不可谓不妙!值得学习~

4.2 接下来看看MeasureSpec是怎么合成和拆解的。
private static final int MODE_SHIFT = 30;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;

这里的图就用10位意思意思了,哈哈


public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << View.MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
画了一副运算的示意图,假用10位代替32位表示
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}
计算获取Size值 计算获取Mode值

五、onMeasure测量方法相关知识?和onMeasure方法中如何进行测量?

5.1 onMeasure传递的参数是自身view的模式和父控件给他的参考高宽值,那么是在哪里修改为子View自身的?

实际上在ViewGroup中有一个方法measureChildWithMargins,会通过父容器传过来的parentMeasureSpec还有Padding、Margin值来计算子控件的childMeasureSpec。

measureChildWithMargins则是由继承至ViewGroup的容器像FrameLayou、LinearLayout等容器在onMeasure方法中调用。(这里修类举FrameLayout)。

还是需要回过头看下《三、View的MeasureSpec创建规则?》,根据父容器的mode模式,在根据子控件设置的具体值、match_parent或者wrap_content来决定子控件的MeasureSpec。

FrameLayout:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

}

ViewGroup:
protected void measureChildWithMargins(View child,
                                       int parentWidthMeasureSpec, int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) {
    final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    ...
    //根据父容器的模式来计算子View的MeasureSpec
    switch (specMode) {
        // EXACTLY模式
        case View.MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // 子控件设置具体宽高值,那么参考值为自己设置的具体值,EXACTLY
                resultSize = childDimension;
                resultMode = View.MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                // 子控件设置match_parent,那么参考值为父容器最大值,EXACTLY
                resultSize = size;
                resultMode = View.MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // 子控件设置wrap_content,那么参考值为父容器最大值,模式为AT_MOST
                resultSize = size;
                resultMode = View.MeasureSpec.AT_MOST;
            }
            break;
        case View.MeasureSpec.AT_MOST:
            ...
            break;
    }
    //noinspection ResourceType
    return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
5.2 onMeasure方法中如何进行测量?

这里实际上是要分为两种,一种是View;一种是ViewGroup。

如果是继承至View的话,那么onMeasure的目的就是测量它自身的宽高;而如果是继承至ViewGroup的话,那么它就是一个自定义容器,它的onMeasure的目的就是通过测量它子View的宽高进而测量自身容器所需的宽高,当然这里还得结合MeasureSpec的模式来区分对待。

5.2.1 自定义View测量步骤:
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
switch (specMode) {
    case View.MeasureSpec.EXACTLY:
        //模式为EXACTLY的时,宽高为父容器给定的参考值(具体值或者父容器最大值)
        resultWidth = specWidthSize;
        resultHeight = specHeightSize;
        break;
    case View.MeasureSpec.AT_MOST:
        //模式为AT_MOST的时,宽高为子View根据自身实际大小计算而来的值
        resultWidth = 为子View根据自身实际大小计算而来的值;
        resultHeight = 为子View根据自身实际大小计算而来的值;
        break;
}
5.2.2 自定义ViewGroup测量步骤:
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
for (int i = 0; i < getChildCount; i++) {
    View child = getChildAt(i);
    measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec);
}

//它也会去根据父容器的MeasureSpec来确定子View的MeasureSpec
protected void measureChild(View child, int parentWidthMeasureSpec,
                            int parentHeightMeasureSpec) {
    final ViewGroup.LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}
switch (specMode) {
    case View.MeasureSpec.EXACTLY:
        //模式为EXACTLY的时,宽高为父容器给定的参考值(具体值或者父容器最大值)
        resultWidth = specWidthSize;
        resultHeight = specHeightSize;
        break;
    case View.MeasureSpec.AT_MOST:
        //模式为AT_MOST的时,容器的宽高需要根据子View宽高计算
        resultWidth = 容器的宽高需要根据子View宽高计算;
        resultHeight = 容器的宽高需要根据子View宽高计算;
        break;
}
setMeasuredDimension(resultWidth, resultHeight);

5.3 getMeasureWidth和getWidth有什么区别?

getMeasureWidth和getWidth最终的到的值一样的,它们只是时机不同而已,getMeasureWidth是在onMeasure方法调用测量完毕后取到值,而getWidth则是在onLayout布局完后取到值。

六、onLayout布局方法的操作流程和应该注意的事项?

布局的话在自定义View中实际上是不需要处理的,只有在自定义ViewGroup的时候才会用到。

其实布局也是很简单的,在上一步onMeasure测量完毕之后,你只需要根据你的设计稿或者你想要的布局方式,给子View设置left、top、right和bottom值就可以了。当然在计算这些值得时候,需要考虑到Margin、Padding值。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

    for (int i = 0; i < getChildCount(); i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;
            ...
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

七、总结

UI的测量、布局和绘制流程就差不多到这了,其实没有具体细致讲到如何真实测量、布局。只是把View的MeasureSpec创建规则以及MeasureSpec的结构等深入源码看了下,我们只需要把原理搞懂了。其实真实用到还是不难的。

自定义View主要是用到onMeasure和onDraw方法。

自定义viewGroup主要是用到了onMeasure和onLayout方法。

上一篇下一篇

猜你喜欢

热点阅读