• Android基础之View的绘制原理


    1.测量

    简单流程就是:

    1. 确定view是树结构
    2. 递归遍历每个子节点,即子view,进行测量。

    下面具体说一下:

    首先view是树结构。
    也就是说子view是父view的孩子节点。
    根节点是就是DecorView。

    了解树的都知道,树的遍历都是递归遍历。
    那么测量view的过程,其实就是遍历树的过程。

    测量什么呢,怎么做一个记录呢。
    就有了MesureSpec。是一个封装的int类型。
    高2位表示mode,其余表示size。

    mode即类型有哪些呢。
    通常我们在定义一个view的宽高时有三种写法。

    一是直接写多少多少dp
    一个是match_parent
    一个是wrap_content

    所以mode也有三种,分别是:
    EXACTLY:对应固定数值的写法,有确切的size。
    AT_MOST:父view给定一个size,子view不超过这个size即可。
    UNSPECIFIED:似乎没有用到。

    所以不同类型的view它的测量方式是不同的。
    比如LinearLayout和FrameLayout具体的测量方式是不同的。

    测量的逻辑(可以先不要看代码,看一下具体逻辑,思考一下怎么实现)

    1. 测量一个view需要知道父view的MeasureSpec。因为需要父view的mode和size来确定子view的mode和size

    2. 根节点没有父view,那么MeasureSpec从哪里获取
      最外层的根节点DecorView的MeasureSpec只由自己的LayoutParams决定
      如果是match_parent和固定数值对应的就是EXACTLY
      如果是wrap_content对应的就是AT_MOST

    对应的方法是

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        //如果是MATCH_PARENT,那么就是EXACTLY
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        //如果是WRAP_CONTENT,就是AT_MOST
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            //如果是固定的值,也是EXACTLY
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    
    1. 从根节点开始,遍历子view
      整体流程是其实就是层次遍历。先计算子view的count,然后for循环,获取每个子view,计算其宽高的MeasureSpce,然后子view也是view吧。再去递归调用view的measure方法实现递归调用。

    这里子view其实分为view和ViewGroup。
    view的话后面不要递归遍历,直接计算就行了。
    如果是ViewGroup还是要层次遍历,确保ViewGroup的子view都被调用到。

    对应的方法是:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
    
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //分析1 : 遍历所有子控件,测量每个子控件的大小
                    //参数1:View控件
                    //参数2:宽MeasureSpec
                    //参数3:父容器在宽度上已经用了多少了,因为FrameLayout的规则是:前面已经放置的View并不会影响后面放置View的宽高,是直接覆盖到上一个View上的.所以这里传0
                    //参数4:高MeasureSpec
                    //参数5:父容器在高度上已经用了多少了
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            }
        }
        ......
    
        //分析2 : 测量完所有的子控件的大小之后,才知道自己的大小  这很符合FrameLayout的规则嘛
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        ......
    }
    

    4.具体计算子view的宽高。

    就是根据父view的MesureMode和子view写的是固定值还是wrap_content或者match_parent来确定子view的mode和size。
    具体方法是:

    //这里来自ViewGroup的getChildMeasureSpec方法,无删减
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //根据父容器的MeasureSpec获取父容器的SpecMode和SpecSize
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        
        //剩下的size
        int size = Math.max(0, specSize - padding);
    
        //最终的size和mode
        int resultSize = 0;
        int resultMode = 0;
    
        switch (specMode) {
        // Parent has imposed an exact size on us
        //父容器有一个确定的大小
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                //子控件也是确定的大小,那么最终的大小就是子控件设置的大小,SpecMode为EXACTLY
                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.
                //子控件想要自己定义大小,但是不能超过剩余空间 size
                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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    

    MeasureSpec

    一个view的MeasureMode怎么确定

    1. 如果该view的宽高是固定值,那么其mode是EXACTLY
    2. 如果view的宽高是MATCH_PARENT,那就继承父view的mode。
      即父view的mode是EXCETLY,该view就是EXACTLY;
      父View是AT_MOST,该view就是AT_MOST;
      父View是UNSPECIFIED,该view也是 UNSPECIFIED。
    3. 如果view的宽高是WRAP_CONTENT。那就不论父view是什么mode,该view都是AT_MOST除了UNSPECIFIED。

    Measure流程图

    image

    2.布局

    思考一个问题,影响布局有哪些因素:

    1. 首先能想到的是上个步骤中测量的宽高
    2. 其次就是Gravity,不同的ViewGroup不一样。比如RelativeLayout和LinearLayout差别就很大。需要具体分析。layout的时候也要考虑。
    • Gravity.BOTTOM
    • Gravity.TOP
    • Gravity.CENTER_VERTICAL
    • Gravity.CENTER_HORIZONTAL
    • Gravity.RIGHT
    • Gravity.LEFT
    1. 就是layout时自定义的几个参数left,top,right,bottom

    布局首先是确定四个参数:
    left,top,right,bottom
    什么意思呢:
    这四个参数位置都是相对于父容器而言的

    那么具体是怎么操作呢:

    1. 首先是从根节点也就是DecorView开始。
      根节点的四个参数如何确定,很简单,如下所示。
    //这里的host其实是根视图(DecorView)
    //参数:left,top,right,bottom  这些位置都是相对于父容器而言的
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    
    1. 根节点之后就开始层次遍历。
      这里子view其实分为view和ViewGroup。view的话后面不要递归遍历,直接计算就行了。如果是ViewGroup还是要层次遍历,确保ViewGroup的子view都被调用到。

    拿到当前view的子view的count,for循环拿到每个子view。根据子view的测量出来的宽高,以及Gravity等参数。去计算子view的四个参数。
    就是该子view相对于父view的布局是什么。
    然后再去递归调用子view的布局方法即layout方法。实现整个递归遍历。

    3.绘制

    绘制又分为两种:
    如果开启并支持硬件绘制加速(从 Android 4.X 开始谷歌已经默认开启硬件加速),则走 GPU 硬件绘制的流程,否则走 CPU 软件绘制的流程。

    先看一下cpu软件绘制流程:
    先看一下官方给的注释:

    /*
            注意了这是官方给的注释,谷歌工程师还真是贴心,把draw步骤写的详详细细,给力,点赞
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
    

    翻译一下就是:

    1. 绘制背景
    2. 绘制控件自己本身的内容
    3. 绘制子控件
    4. 绘制装饰(比如滚动条)和前景

    整体流程比较简单:
    还是从ViewRootImpl的performTraversals方法开始分析

    private void performTraversals() {
        //开始绘画流程
        performDraw();
    }
    
    private void performDraw() {
        ......
        draw(fullRedrawNeeded);
        ......
    }
    
    private void draw(boolean fullRedrawNeeded){
        .....
        drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty);
        .....
    }
    
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
        ......
        mView.draw(canvas);
        ......
    }
    
    

    随着方法的调用深入,发现来到了View的draw方法

    public void draw(Canvas canvas) {
        .....
    
        /*
            注意了这是官方给的注释,谷歌工程师还真是贴心,把draw步骤写的详详细细,给力,点赞
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
    
        // Step 1, draw the background, if needed
        //1. 绘制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
    
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            //3. 绘制自己的内容
            if (!dirtyOpaque) onDraw(canvas);
    
            // Step 4, draw the children
            //4. 绘制子控件  如果是View的话这个方法是空实现,如果是ViewGroup则绘制子控件
            dispatchDraw(canvas);
    
            drawAutofilledHighlight(canvas);
    
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
    
            // Step 6, draw decorations (foreground, scrollbars)
            //6. 绘制装饰和前景
            onDrawForeground(canvas);
    
            // Step 7, draw the default focus highlight
            //7. 绘制默认焦点高亮显示
            drawDefaultFocusHighlight(canvas);
    
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }
    
            // we're done...
            return;
        }
        .....
    }
    

    总结

    绘制完毕之后干嘛呢。会交个RenderThread处理,然后交给SurfaceFlinger,最后显示到屏幕上。
    这部分后面继续分析。

    参考文档

    死磕Android_View工作原理你需要知道的一切 (github需要科(这)学(也)上(是)网(敏感词?!!))

  • 相关阅读:
    Castle.Aop.Autofac
    signalR 在webfarm下的配置
    SQL语句中 string类型数字的比较
    access 查询空字段
    c#利用jmail发送邮件
    C# 利用Jmail接收邮件
    Asp.net 使用 AXAJ局部刷新无效的解决方法
    把查询的数据放入多维数组中
    获取网站的根目录的物理文件系统路径
    C#.net如何生成静态页带母板的那种
  • 原文地址:https://www.cnblogs.com/cfdroid/p/16427133.html
Copyright © 2020-2023  润新知