zoukankan      html  css  js  c++  java
  • Android开发——View绘制过程源码解析(一)

    0. 前言  

    View的绘制流程从ViewRootperformTraversals开始,经过measurelayoutdraw三个流程,之后就可以在屏幕上看到View了。其中measure用于测量View的宽和高layout用于确定View在父容器中放置的位置draw则用于将View绘制到屏幕上

    本文原创,转载请注明出处:SEU_Calvin的CSDN博客


    1. MeasureSpec

    说到measure那么就不得不提MeasureSpec,它是由ViewLayoutParams和父容器(顶级View则为屏幕尺寸)一起决定的,一旦确定了MeasureSpec,在onMeasure()中就可以确定View的宽高

    MeasureSpec的值由SpecSize(测量值)和SpecMode(测量模式)共同组成。

    SpecMode一共有三种类型:

    1EXACTLY表示设置了精确的值,一般当childView设置其宽高为精确值、match_parent(同时父容器也是这种模式)时,ViewGroup会将其设置为EXACTLY

    2AT_MOST表示子布局被限制在一个最大值内(即SpecSize),一般当childView设置其宽高为wrap_contentmatch_parent(同时父容器也是这种模式)时,ViewGroup会将其设置为AT_MOST

    3UNSPECIFIED表示View可以设置成任意的大小,没有任何限制。这种情况比较少见。

     

    2. MeasureSpec的生成过程

    2.1 顶级ViewMeasureSpec

    // desiredWindowWidth和desiredWindowHeight为屏幕尺寸
    // lp.width和lp.height都等于MATCH_PARENT
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 
    //…
    private int getRootMeasureSpec(int windowSize, int rootDimension) {  
        int measureSpec;  
        switch (rootDimension) {  
        case ViewGroup.LayoutParams.MATCH_PARENT:  
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
            break;  
        case ViewGroup.LayoutParams.WRAP_CONTENT:  
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
            break;  
        default:  
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
            break;  
        }  
        return measureSpec;  
    }  
    

    从源码中可以看出,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpecrootDimension参数等于MATCH_PARENTMeasureSpecSpecModeEXACTLY。并且MATCH_PARENTWRAP_CONTENT时的SpecSize都等于windowSize的,也就意味着根视图总是会充满全屏的。

     

    2.2 普通ViewMeasureSpec

    在对子元素进行measure之前,会先调用getChildMeasureSpec方法得到子元素的MeasureSpec。上面也提到过了,子元素MeasureSpec与父容器的MeasureSpec和子元素本身的LayoutParams有关。源码介绍如下:

    //参1为父容器的MeasureSpec
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // Parent has imposed an exact size on us
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent has imposed a maximum size on us
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed.
                    // Constrain child to not be bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how
                    // big it should be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    
    

    其中上述源码总结如下:

    1)当View为固定宽高时,MeasureSpecEXACTLY模式/LayoutParams中的大小。

    2)当ViewWRAP_CONTENT时,MeasureSpecAT_MOST模式/父容器剩余空间大小。

    3)当ViewMATCH_PARENT时,若父容器为EXACTLY模式,那么ViewMeasureSpecEXACTLY模式/父容器的剩余大小。若父容器为AT_MOST模式,那么ViewMeasureSpecAT_MOST模式/父容器的剩余大小。


    3. Measure过程

    3.1 普通ViewMeasure过程

    Viewmeasure()方法是final的,因此我们无法在子类中去重写这个方法,在该方法内部会调用onMeasure()方法,源码如下所示。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // setMeasuredDimension设置视图的大小,这样就完成了一次measure的过程
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),    
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    
    }
    //这个方法就是近似的返回spec中的specSize,除非你的specMode是UNSPECIFIED
    //UNSPECIFIED 这个一般都是系统内部测量才用的到
    public static int getDefaultSize(int size, int measureSpec) {
            int result = size;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
    }
    

    onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。

     

    getDefaultSize方法中,从上面源码的11-20行,结合2.2中的结论可以知道:

    (1)如果设置了固定宽高,View是EXACTLY模式并且传入MeasureSpec的大小就是自定义的宽高,上述11行、18行和19行代码表示这种设置会显示正常。

    (2)如果设置了MATCH_PARENT,View的MODE会有两种情况,不过不管是哪一种,结果都是从MeasureSpec中获取大小,通过2.2中的结论可知为父容器剩余大小,因此这种设置逻辑上也会显示正常。

    (3)如果设置了WRAP_CONTENT,View的MODE一定会是AT_MOST,结果是从MeasureSpec中获取大小,通过2.2中的结论可知为父容器剩余大小,逻辑上就会显示不正常,包裹内容效果会失效。这里就不贴实例了,网上有很多,有兴趣可以查看这一篇


    说了这么多,我们得出的结论就是:直接继承View的自定义控件需要重写onMeasure()并设置WRAP_CONTENT时自身大小。

    解决方式就是在onMeasure里针对WRAP_CONTENT来做特殊处理,比如通过setMeasuredDimension()指定一个默认的宽高。逻辑如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //默认值
        int desiredWidth = 100;
        int desiredHeight = 100;
    
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
        int width;
        int height;
    
        //Measure Width
        if (widthMode == MeasureSpec.EXACTLY) {
            //Must be this size
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            //Can't be bigger than...
            width = Math.min(desiredWidth, widthSize);
        } else {
            //Be whatever you want
            width = desiredWidth;
        }
    
        //Measure Height
        if (heightMode == MeasureSpec.EXACTLY) {
            //Must be this size
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            //Can't be bigger than...
            height = Math.min(desiredHeight, heightSize);
        } else {
            //Be whatever you want
            height = desiredHeight;
        }
    
        //MUST CALL THIS
        setMeasuredDimension(width, height);
    }
    

    我翻了所有的资料,重写该方法时貌似默认AT_MOST就等于WRAP_CONTENT。我们知道顶级容器默认是EXACTLY模式,所以在这篇博客里的例子中上述代码可以解决WRAP_CONTENT失效的问题。但是如果布局参数写为MATCH_PARENT但是父容器为AT_MOST模式时,得出的子View也是AT_MOST模式,那么上述代码好像是有逻辑漏洞的。想了想,好像确实很难出现这种情况,具体不太清楚,有清楚的朋友可以留言交流一下。

     

    3.2 ViewGroupMeasure过程

    因为一个布局中一般都会包含多个子视图,因此每个视图都需要经历一次measure过程。

    ViewGroup是没有onMeasure方法的,这个方法是交给子类自己实现的。不同的ViewGroup子类布局都不一样,那么onMeasure索性就全部交给他们自己实现好了。

    ViewGroup中定义了一个measureChildren()方法来遍历测量子视图的大小,如下所示:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
        final int size = mChildrenCount;  
        final View[] children = mChildren;  
        for (int i = 0; i < size; ++i) {  
            final View child = children[i];  
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
                measureChild(child, widthMeasureSpec, heightMeasureSpec);  
            }  
        }  
    } 
    

    里面循环调用了measureChild,其实现为:

    protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {    
           //通过子布局参数和父容器MeasureSpec得到childMeasureSpec
           //过程略..
           //所以这其实是个递归调用,不断的去测量设置子视图的大小,直至完成整个View数测量遍历 
           child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    
    }
    


    以上就是关于measure过程的一些解析,后面会更新另外layout和draw过程的解析。

    本文原创,转载请注明出处SEU_Calvin的CSDN博客。欢迎留言交流,谢谢。



  • 相关阅读:
    Android之上下文context
    如果简单的记录,就可以为这个世界创造更多的财富,那么还有什么理由不去写博客呢? — 读<<黑客与画家>> 有感
    【最新最全】为 iOS 和 Android 的真机和模拟器编译 Luajit 库
    【Graphql实践】使用 Apollo(iOS) 访问 Github 的 Graphql API
    【最新】LuaJIT 32/64 位字节码,从编译到使用全纪录
    简陋的swift carthage copy-frameworks 辅助脚本
    【自问自答】关于 Swift 的几个疑问
    【读书笔记】The Swift Programming Language (Swift 4.0.3)
    【读书笔记】A Swift Tour
    【趣味连载】攻城狮上传视频与普通人上传视频:(一)生成结构化数据
  • 原文地址:https://www.cnblogs.com/qitian1/p/6461498.html
Copyright © 2011-2022 走看看