美文网首页
UI的测量、布局和绘制源码详解

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

作者: 天上飘的是浮云 | 来源:发表于2022-02-05 09:49 被阅读0次

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

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

    • 1、View的MeasureSpec创建规则?
    • 2、MeasureSpec的含义以及它的结构是怎样的?
    • 3、onMeasure测量方法相关知识?
    • 4、onLayout布局方法的操作流程和应该注意的事项?

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

    2.1 首先ViewRootImpl是由WindowManagerGlobal实例化的,而WindowManagerGlobal是单例,
    2.2 ViewRootImpl的performMeasure、performLayout和performDraw最终都是调用View的measure、layout和draw方法。
    2.3 我们再看看View中的requestLayout、invalidate和postInvalidate最终都是调用了谁?
      1. View.requestLayout ---> mParent.requestLayout(mParent是ViewParent,它是一个接口而ViewRootImpl实现了它。并在ViewRootImpl.setView中通过view.assignParent(this)给其赋值) ---> ViewRootImpl.requestLayout ---> ViewRootImpl.scheduleTraversals(最终回到ViewRootImpl通过Choreographer编舞者类去底层获取Vsync垂直同步信号的地方了。)
      1. View.invalidate ---> View.invalidateInternal ---> p.invalidateChild(p是ViewParent,它是一个接口而ViewRootImpl实现了它。并在ViewRootImpl.setView中通过view.assignParent(this)给其赋值) ---> ViewRootImpl.invalidateChild ---> ViewRootImpl.invalidateChildInParent ---> ViewRootImpl.invalidateRectOnScreen ---> ViewRootImpl.scheduleTraversals(最终回到ViewRootImpl通过Choreographer编舞者类去底层获取Vsync垂直同步信号的地方了。)
      1. View.postInvalidate ---> View.postInvalidateDelayed ---> ViewRootImpl.dispatchInvalidateDelayed ---> ViewRootImpl.mHandler(发送的Handler消息为MSG_INVALIDATE) ---> ViewRootImpl.handleMessage(((View) msg.obj).invalidate();) ---> 其实又回到了第2点从View.invalidate, postInvalidate只是通过Handler机制加了一个延时消息并切换为主线程

    三、View的MeasureSpec创建规则?

    看图说话
    3.1 当父容器为EXACTLY模式时:
      1. 如果子View指定了具体宽高值时,那么这个子View的resultSize(参考值)就是你给予的具体值,模式为EXACTLY。
      1. 如果子View指定是match_parent时,那么子View的resultSize(参考值)为父容器给予的最大值,模式为EXACTLY。
      1. 如果子View指定的是wrap_content时,那么子View的resultSize(参考值)还是为父容器给予的最大自,模式变为AT_MOST。
    3.2 当父容器为AT_MOST模式时:
      1. 如果子View指定了具体宽高值时,那么这个子View的resultSize(参考值)就是你给予的具体值,模式为EXACTLY。
      1. 如果子View指定的是match_parent时,那么子View的resultSize(参考值)为父容器给予的最大值,模式为AT_MOST。
      1. 如果子View指定的是wrap_content时,那么子View的resultSize(参考值)也为父容器给予的最大值,模式也为AT_MOST。
    3.3 当父容器为UNSPECIFIED模式时:
      1. 如果子View制定了具体的宽高值时,那么这个子View的resultSize(参考值)就是你给予的具体值,模式为EXACTLY。
      1. 如果子View为match_parent或wrap_content时,那么resultSize(参考值)都为0, 模式都为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是怎么合成和拆解的。
      1. 首先,MeasureSpec是个内部类,其实很少代码,主要是用位运算来处理。因为位运算的效率是很高的。(为什么会很高,这就和JVM指令集相关了,直接有位运算的指令,你说快不快)。直接看代码,UNSPECIFIED就是00左移30位,EXACTLY就是01左移30位,而AT_MOST就是10左移30位。
    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位意思意思了,哈哈


      1. 而mode和size的合成其实也很简单,也是位运算、与运算和或运算。size先与非MODE_MASK与运算,再与mode和MODE_MASK与运算的结果进行或运算。
    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位表示
      1. 接下来通过MeasureSpec来分别获得mode和size值就更简单了。
    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测量步骤:
      1. 在onMeasure方法中通过传入的参数来分别获取mode和size。
    int specMode = MeasureSpec.getMode(widthMeasureSpec);
    int specSize = MeasureSpec.getSize(widthMeasureSpec);
    
      1. 然后通过自身mode模式不同来设置宽高值
    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测量步骤:
      1. 在onMeasure方法中通过传入的参数来分别获取mode和size。
    int specMode = MeasureSpec.getMode(widthMeasureSpec);
    int specSize = MeasureSpec.getSize(widthMeasureSpec);
    
      1. 测量子View的宽高
    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);
    }
    
      1. 在自定义ViewGroup时,可能要获取Margin值之类,这时候需要重写generateLayoutParams方法
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
    
      1. 然后通过自身mode模式不同来设置宽高值
    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;
    }
    
      1. 最后通过setMeasuredDimension将宽高设置进去
    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方法。

    相关文章

      网友评论

          本文标题:UI的测量、布局和绘制源码详解

          本文链接:https://www.haomeiwen.com/subject/zehjkrtx.html