美文网首页
View:绘制流程

View:绘制流程

作者: 9283856ddec1 | 来源:发表于2020-03-12 13:09 被阅读0次

    1 Android视图层次结构

    视图层次结构.png

    上图是针对比较老的Android系统版本中制作的,新的版本中会略有出入,但整体上没变。平时在Activity中setContentView(...)时,添加到“+id/content”的FrameLayout上,自己的布局对应的是上图中ViewGrop的树状结构。

    • Window概念
      Window表示的是一个窗口的概念,它是站在WindowManagerService角度上的一个抽象的概念,Android中所有的视图都是通过Window来呈现的,不管是Activity、Dialog还是Toast,只要有View的地方就一定有Window。
      注意:抽象的Window概念和PhoneWindow这个类并不是同一个东西,PhoneWindow表示的是手机屏幕的抽象,它充当Activity和DecorView之间的媒介,就算没有PhoneWindow也是可以展示View的。

    • DecorView概念
      DecorView是整个Window界面的最顶层View,View的测量、布局、绘制、事件分发都是由DecorView往下遍历这个View树。DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况和Android的版本及主题有关),上面是【标题栏】,下面是【内容栏】。在Activity中我们通过setContentView所设置的布局文件其实就是被加载到【内容栏】中的,而内容栏的id是content,因此指定布局的方法叫setContent()。

    • ViewRoot概念
      ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完之后,会将DecorView添加到Window中,同时会创建对应的ViewRootImpl,并将ViewRootImpl和DecorView建立关联,并保存到WindowManagerGlobal对象中。

    2 绘制的起源点

    Activity启动在ActivityThread.java类中完成,期间会调用到handleResumeActivity(...)方法,这个方法是View绘制的起源点,整个调用链如下图所示:


    View绘制起源时序图.png

    2.1 handleResumeActivity()

    关键代码如下:

    //=== ActivityThread.java ===
    final void handleResumeActivity(...) {
        ......
        //跟踪代码后发现其初始赋值为mWindow = new PhoneWindow(this, window, activityConfigCallback);
        r.window = r.activity.getWindow(); 
        //从PhoneWindow实例中获取DecorView  
        View decor = r.window.getDecorView();
        ......
        //跟踪代码后发现,vm值为上述PhoneWindow实例中获取的WindowManager。
        ViewManager wm = a.getWindowManager();
        ......
        //当前window的属性,从代码跟踪来看是PhoneWindow窗口的属性
        WindowManager.LayoutParams l = r.window.getAttributes();
        ......
        wm.addView(decor, l);
        ......
    }
    

    ViewManager是一个接口,addView是其定义的一个方法,其实现类为WindowManagerImpl。wm.addView(decor, l)中两个参数会层层传递,直到ViewRootImpl类中。下面分析下这个两个参数由来。

    2.1.1 参数decor

    //=== PhoneWindow.java ===
    
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    ......
    
    public PhoneWindow(...){
       ......
       mDecor = (DecorView) preservedWindow.getDecorView();
       ......
    }
    
    @Override
    public final View getDecorView() {
       ......
       return mDecor;
    }
    

    decor是DecorView实例,它是window的顶级视图。其类继承关系为:DecorView -> FrameLayout -> ViewGroup -> View

    2.1.2 参数l

    //=== Window.java ===
    private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
    ......
    public final WindowManager.LayoutParams getAttributes() {
        return mWindowAttributes;
    }
    
    //=== WindowManager内部类LayoutParams ===
    public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
        public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            ......
        }
    }
    
    //=== ViewGroup.java内部类LayoutParams ===
    public LayoutParams(int width, int height) {
        this.width = width;
        this.height = height;
    }
    

    参数l:表示PhoneWindow的LayoutParams属性,其width和height值均为LayoutParams.MATCH_PARENT。

    2.2 performTraversals()

    performTraversals方法有大约800多行代码,控制着整个绘制流程,关键代码如下:

    // === ViewRootImpl.java ===
    private void performTraversals() {
       ......
       int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
       int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
       ......
       // Ask host how big it wants to be
       performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
       ......
       performLayout(lp, mWidth, mHeight);
       ......
       performDraw();
    }
    

    上述代码是绘制流程的完成过程,涉及三个步骤:
    1)performMeasure():从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度;

    2)performLayout():从根节点向下遍历View树,完成所有ViewGroup和View的布局计算工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域;

    3)performDraw():从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的显示区域,将所有View的当前需显示的内容画到屏幕上。

    performTraversals整体过程如下图所示:


    performTraversals整体绘制示意图.png

    3 View绘制的三个流程

    一个完整的绘制流程包括measure、layout、draw三个步骤,其中:

    • measure(测量)
      系统会先根据xml布局文件和代码中对控件属性的设置,来计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。
    • layout(布局)
      根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。
    • draw(绘制)
      确定好位置后,就将这些控件绘制到屏幕上。

    3.1 measure过程分析

    3.1.1 MeasureSpec介绍

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        ......
        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;
    
        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        ......
       
        /**
         * Creates a measure specification based on the supplied size and mode.
         *...... 
         *@return the measure specification based on size and mode        
         */
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
            ...... 
        }
        
        ......
        /**
         * Extracts the mode from the supplied measure specification.
         *......
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
    
        /**
         * Extracts the size from the supplied measure specification.
         *......
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        ......
    }
    
    1. MeasureSpec含义
      MeasureSpec概括了从父布局传递给子view布局要求。MeasureSpec是32位的int值,高2位代表SpecMode(模式),低30位代表SepcSize(尺寸),这样的打包方式好处是避免过多的对象内存分配。其结构示意图如下:


      MeasureSpec结构示意图.png
    2. 三种模式

    • UNSPECIFIED:未指定尺寸模式。父容器不对View有任何限制,要多大就给多大。(笔者注:这个在工作中极少碰到,据说一般在系统中才会用到,后续会讲得很少)
    • EXACTLY:精确值模式。父布局决定了子view的准确尺寸,子view无论想设置多大的值,都将限定在那个边界内。
    • AT_MOST:最大值模式。子view可以一直大到指定的值。(笔者注:宽高属性设置为wrap_content,那么它的最大值不会超过父布局给定的值,所以称为最大值模式)


      三种mode示意图.png
    1. 主要方法
    方法 含义
    makeMeasureSpec 用于将mode和size打包成一个int型的MeasureSpec
    getMode 从指定的measureSpec值中获取其mode
    getSize 从指定的measureSpec值中获取其size

    3.1.2 ViewGroup.LayoutParams介绍

    //=== ViewGroup.java ===
    public static class LayoutParams {
        ......
    
        /**
         * Special value for the height or width requested by a View.
         * MATCH_PARENT means that the view wants to be as big as its parent,
         * minus the parent's padding, if any. Introduced in API Level 8.
         */
        public static final int MATCH_PARENT = -1;
    
        /**
         * Special value for the height or width requested by a View.
         * WRAP_CONTENT means that the view wants to be just large enough to fit
         * its own internal content, taking its own padding into account.
         */
        public static final int WRAP_CONTENT = -2;
    
        /**
         * Information about how wide the view wants to be. Can be one of the
         * constants FILL_PARENT (replaced by MATCH_PARENT
         * in API Level 8) or WRAP_CONTENT, or an exact size.
         */
        public int width;
    
        /**
         * Information about how tall the view wants to be. Can be one of the
         * constants FILL_PARENT (replaced by MATCH_PARENT
         * in API Level 8) or WRAP_CONTENT, or an exact size.
         */
        public int height;
        ......
    }
    
    1. LayoutParams含义
      View用LayoutParams告诉父布局,它们想要怎样被布局。其width和height属性对应着layout_width和layout_height属性。ViewGroup不同的子类,会定义出不同LayoutParams子类。

    2. LayoutParams取值
      LayoutParams指定三种数值:MATCH_PARENT、WRAP_CONTENT、具体数值;

    • MATCH_PARENT:该view希望和父布局尺寸一样大。
    • WRAP_CONTENT:该view希望其大小为仅仅足够包裹住其内容即可。

    3.1.3 View测量流程

    3.1.3.1 ViewRootImpl.performMeasure()
    //=== ViewRootImpl.java ===
    public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
        public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
              ......
              mView = view;
              ......
              mWindowAttributes.copyFrom(attrs);
              ......
        }
        
        private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
               ......
               mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
               ......
        }
    }
    

    ① setView的参数view和attrs是ActivityThread类中addView方法传递过来的,可以确定mView指的是DecorView。
    ② 在performMeasure()中,其实是DecorView在执行measure()操作。如果您这存在“mView不是View类型的吗,怎么会指代DecorView作为整个View体系的根view呢”这样的疑惑,那这里就啰嗦一下,DecorView extends FrameLayout extends ViewGroup extends View,通过这个继承链可以看到,DecorView是一个容器,但ViewGroup也是View的子类,View是所有控件的基类,所以这里View类型的mView指代DecorView是没毛病的。

    childWidthMeasureSpec和childHeightMeasureSpec由来

    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    

    getRootMeasureSpec(int,int)方法的完整源码如下所示:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    ① 基于window的layout params,在window中为root view获取measureSpec。
    ② 参数windowSize:window的可用宽度和高度值;参数rootDimension:window的宽/高的layout param值。

    3.1.3.2 View.measure()

    尽管mView就是DecorView,但是由于measure()方法是final型的,View子类都不能重写该方法,所以这里追踪measure()的时候就直接进入到View类中了,这里贴出关键流程代码:

    //=== View.java ===
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
          ......
          // measure ourselves, this should set the measured dimension flag back
          onMeasure(widthMeasureSpec, heightMeasureSpec);
          ......
    }
    

    ① 系统将measure方法定义为final,说明系统不希望整个测量流程框架被修改。
    ② view的实际测量工作放在onMeasure方法实现的??
    ③ 参数widthMeasureSpec:父布局加入的水平空间要求;参数heightMeasureSpec:父布局加入的垂直空间要求。

    3.1.3.3 View.onMeasure()
    //=== View.java ===
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    

    ① 当重写该方法时,必须调用setMeasuredDimension(int,int)来存储该view测量出的宽和高,否则会触发IllegalStateException,由measure(int,int)抛出。调用基类的onMeasure(int,int)方法是一个有效的方法。
    ② ViewGroup的子类必须重写该方法,才能绘制该容器内的子view。如果是自定义一个子控件,extends View,那么并不是必须重写该方法;
    ③ 容器类控件都是ViewGroup的子类,如FrameLayout、LinearLayout等,都会重写onMeasure方法,根据自己的特性来进行测量;如果是叶子节点view,即最里层的控件,如TextView等,也可能会重写onMeasure方法,所以当流程走到onMeasure(...)时,流程可能就会切到那些重写的onMeasure()方法中去。
    ④ widthMeasureSpec:父布局加入的水平空间要求;heightMeasureSpec:父布局加入的垂直空间要求。
    ⑤ 如果该方法被重写,子类负责确保测量的高和宽至少是该view的mininum高度和mininum宽度值(链接getSuggestedMininumHeight()和getSuggestedMininumWidth());

    getSuggestedMinimumWidth()方法

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    

    "mininum width“指的是在xml布局文件中该view的“android:minWidth"属性值,“background's minimum width”值是指“android:background”的宽度。该方法的返回值就是两者之间较大的那一个值,用来作为该view的最小宽度值。现在应该很容易理解了吧,当一个view在layout文件中同时设置了这两个属性时,为了两个条件都满足,自然要选择值大一点的那个了。

    getDefaultSize()方法

    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;
    }
    

    ① 参数由widthMeasureSpec变成了measuredWidth,即由“父布局加入的水平空间要求”转变为了view的宽度,measuredHeigh也是一样。
    ② 如果父布局没有施加任何限制,即MeasureSpec的mode为UNSPECIFIED,那么返回值为参数中提供的size值。如果父布局施加了限制,则返回的默认尺寸为保存在参数measureSpec中的specSize值。

    3.1.3.4 View.setMeasuredDimension()
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        ......
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    

    ① measuredWidth:该view被测量出宽度值;measuredHeight:该view被测量出的高度值。

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        ......
    }
    

    View中的成员变量mMeasureWidth和mMeasureHeight就被赋值了,这也就意味着,View的测量就结束了。

    //=== View.java ===
    public static final int MEASURED_SIZE_MASK = 0x00ffffff;
    
    public final int getMeasuredWidth() {
       return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
    
    public final int getMeasuredHeight() {
       return mMeasuredHeight & MEASURED_SIZE_MASK;
    }
    

    ① 获取原始的测量宽度和高度,这两个方法需在setMeasuredDimension()方法执行后才有效,否则返回值为0。

    3.1.4 DecorView测量过程

    3.1.4.1 DecorView.onMeasure()
    //=== DecorView.java ===
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }
    
    //=== FrameLayout.java ===
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            ......
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            ......             
        }
        ......
        setMeasuredDimension(......)
    }
    

    ① DecorView的继承链:DecorView extends FrameLayout extends ViewGroup extends View。当DecorView第一次调用到measure()方法后,流程就开始切换到重写的onMeasure()中。DecorView在onMeasure()方法做一些事项后,调用父类的onMeasure方法。
    ② FrameLayout对OnMeasure()方法进行重写,当所有子view测量完成后,最后调用setMeasuredDimension(...)来测量自己的。

    3.1.4.2 ViewGroup.measureChildWithMargins()

    measureChild()方法和measureChildWithMargins()

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final 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);
    }
    
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (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);
    }
    

    ① measureChildWithMargins在measureChild的基础上增加:已使用的宽高、margin值。其实它们的功能都是一样的,最后都是生成子View的MeasureSpec,并传递给子View继续测量,即最后一句代码child.measure(childWidthMeasureSpec, childHeightMeasureSpec)。
    ② 在FrameLayout和LinearLayout中重写的onMeasure方法中调用的就是后者,而AbsoluteLayout中就是间接地调用的前者。而RelativeLayout中,两者都没有调用,而是自己写了一套方法,不过该方法和后者方法仅略有差别,但基本功能还是一样。

    getChildMeasureSpec()方法
    目的:将父布局传递来的MeasureSpec和其子view的LayoutParams,整合成子View的MeasureSpec。

    // spec参数   表示父View的MeasureSpec 
    // padding参数    父View的Padding+子View的Margin,父View的大小减去这些边距,才能精确算出
    //               子View的MeasureSpec的size
    // childDimension参数  表示该子View内部LayoutParams属性的值(lp.width或者lp.height)
    //                    可以是wrap_content、match_parent、一个精确指(an exactly size),  
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
        int specMode = MeasureSpec.getMode(spec);  //获得父View的mode  
        int specSize = MeasureSpec.getSize(spec);  //获得父View的大小  
    
       //父View的大小-自己的Padding+子View的Margin,得到值才是子View的大小。
        int size = Math.max(0, specSize - padding);   
      
        int resultSize = 0;    //初始化值,最后通过这个两个值生成子View的MeasureSpec
        int resultMode = 0;    //初始化值,最后通过这个两个值生成子View的MeasureSpec
      
        switch (specMode) {  
        // Parent has imposed an exact size on us  
        //1、父View是EXACTLY的 !  
        case MeasureSpec.EXACTLY:   
            //1.1、子View的width或height是个精确值 (an exactly size)  
            if (childDimension >= 0) {            
                resultSize = childDimension;         //size为精确值  
                resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。  
            }   
            //1.2、子View的width或height为 MATCH_PARENT/FILL_PARENT   
            else if (childDimension == LayoutParams.MATCH_PARENT) {  
                // Child wants to be our size. So be it.  
                resultSize = size;                   //size为父视图大小  
                resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。  
            }   
            //1.3、子View的width或height为 WRAP_CONTENT  
            else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                // Child wants to determine its own size. It can't be  
                // bigger than us.  
                resultSize = size;                   //size为父视图大小  
                resultMode = MeasureSpec.AT_MOST;    //mode为AT_MOST 。  
            }  
            break;  
      
        // Parent has imposed a maximum size on us  
        //2、父View是AT_MOST的 !      
        case MeasureSpec.AT_MOST:  
            //2.1、子View的width或height是个精确值 (an exactly size)  
            if (childDimension >= 0) {  
                // Child wants a specific size... so be it  
                resultSize = childDimension;        //size为精确值  
                resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY 。  
            }  
            //2.2、子View的width或height为 MATCH_PARENT/FILL_PARENT  
            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;                  //size为父视图大小  
                resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST  
            }  
            //2.3、子View的width或height为 WRAP_CONTENT  
            else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                // Child wants to determine its own size. It can't be  
                // bigger than us.  
                resultSize = size;                  //size为父视图大小  
                resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST  
            }  
            break;  
      
        // Parent asked to see how big we want to be  
        //3、父View是UNSPECIFIED的 !  
        case MeasureSpec.UNSPECIFIED:  
            //3.1、子View的width或height是个精确值 (an exactly size)  
            if (childDimension >= 0) {  
                // Child wants a specific size... let him have it  
                resultSize = childDimension;        //size为精确值  
                resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY  
            }  
            //3.2、子View的width或height为 MATCH_PARENT/FILL_PARENT  
            else if (childDimension == LayoutParams.MATCH_PARENT) {  
                // Child wants to be our size... find out how big it should  
                // be  
                resultSize = 0;                        //size为0! ,其值未定  
                resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED  
            }   
            //3.3、子View的width或height为 WRAP_CONTENT  
            else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                // Child wants to determine its own size.... find out how  
                // big it should be  
                resultSize = 0;                        //size为0! ,其值未定  
                resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED  
            }  
            break;  
        }  
        //根据上面逻辑条件获取的mode和size构建MeasureSpec对象。  
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
    }    
    

    pecMode和specSize分别是父布局传下来的要求,size的值是父布局尺寸要求减去其padding值,最小不会小于0。代码最后就是将重新得到的mode和size组合生成一个新的MeasureSpec,传递给子View,一直递归下去。本段代码重难点就是这里新mode和新size值的确定,specMode和childDimension各有3种值,所以最后会有9种组合。总结图形如下:


    父规格与子布局的组合形式.png
    • 如果specMode的值为MeasureSpec.EXACTLY,即父布局对子view的尺寸要求是一个精确值,这有两种情况,父布局中layout_width属性值被设置为具体值,或者match_parent,它们都被定义为精确值。子view的childDimension讨论:
      ① childDimension值为具体数值时,此时resultSize为childDimension的精确值,resultMode理所当然为MeasureSpec.EXACTLY。这里不知道读者会不会又疑问,如果子View的layout_width值比父布局的大,那这个结论还成立吗?按照我们的经验,似乎不太能理解,因为子view的宽度再怎么样也不会比父布局大。事实上,我们平时经验看到的,是最后布局后绘制出来的结果,而当前步骤为测量值,是有差别的。读者可以自定义一个View,将父布局layout_width设置为100px,该自定义的子view则设置为200px,然后在子view中重写的onMeasure方法中打印出getMeasuredWidth()值看看,其值一定是200。甚至如果子view设置的值超过屏幕尺寸,其打印值也是设置的值。
      ② childDimension值为LayoutParams.MATCH_PARENT时。这个容易理解,它的尺寸和父布局一样,也是个精确值,所以resultSize为前面求出的size值,由父布局决定,resultMode为MeasureSpec.EXACTLY。
      ③ childDimension值为LayoutParams.WRAP_CONTENT时。当子view的layout_width被设置为wrap_content时,子view最多能够达到父视图的大小,所以resultSize值为size大小,resultMode为MeasureSpec.AT_MOST。
    • 如果specMode值为MeasureSpec.AT_MOST,父视图对应于layout_width为wrap_content。子view的childDimension讨论:
      ① childDimension为精确值时。很容易明确specSize为自身的精确值,specMode为MeasureSpec.EXACTLY。
      ② childDimension为LayoutParams.MATCH_PARENT时。specSize由父布局决定,specSize为size,specMode为MeasureSpec.AT_MOST。
      ③ childDimension为LayoutParams.WRAP_CONTENT时。specSize由父布局决定,specSize为size,specMode为MeasureSpec.AT_MOST。
    • 如果specMode值为MeasureSpec.UNSPECIFIED。前面说过,平时很少用,一般用在系统中,不过这里还是简单说明一下。这一段有个变量View.sUseZeroUnspecifiedMeasureSpec,它是用于表示当前的目标api是否低于23(对应系统版本为Android M)的,低于23则为true,否则为false。现在系统版本基本上都是Android M及以上的,所以这里该值我们当成false来处理。
      ① childDimension为精确值时。很容易明确specSize为自身的精确值,specMode为MeasureSpec.EXACTLY。
      ② childDimension为LayoutParams.MATCH_PARENT时。specSize由父布局决定为size,specMode和父布局一样,为MeasureSpec.UNSPECIFIED。
      ③ childDimension为LayoutParams.WRAP_CONTENT时。specSize由父布局决定为size,specMode和父布局一样,为MeasureSpec.UNSPECIFIED。
    3.1.4.3 DecorView视图树measure流程图
    --.png

    3.2 layout过程分析

    3.2.1 View布局流程

    3.2.1.1 ViewRootImpl.performLayout()
    //=== ViewRootImpl.java ===
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
       ......
       final View host = mView;
       ......
       host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
       ......
    }
    

    mView就是DecorView,lp中width和height均为LayoutParams.MATCH_PARENT。

    3.2.1.2 ViewGroup.layout()
    //=== ViewGroup.java ===
    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }
    

    ① 由于DecorView是一个容器,是ViewGroup子类,所以跟踪代码的时候,实际上是先进入到ViewGroup类中的layout方法中。在layout方法中调用View.layout()方法。
    ② layout方法是final的,说明系统不希望自定的ViewGroup子类破坏layout流程。

    3.2.1.3 View.layout()
    /**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    @SuppressWarnings({"unchecked"})
    public void layout(int l, int t, int r, int b) {
        ......
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);  
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ......
        }
        ......
    }
    

    目的 :根据子视图的大小以及布局参数将View树放到合适的位置上。
    ① 给view和它的所有后代分配尺寸和位置。
    ② 派生类不应该重写该方法,容器类应该重写onLayout方法。在重写的onLayout方法中,它们应该为每一子view调用layout方法进行布局。
    ③ 参数依次为:Left、Top、Right、Bottom四个点相对父布局的位置。
    ④ setOpticalFrame方法最后会调用setFrame方法,将布局信息进行保持。

    setFrame方法

    //=================View.java================
    /**
     * Assign a size and position to this view.
     *
     * This is called from layout.
     *
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     * @return true if the new size and position are different than the
     *         previous ones
     * {@hide}
     */
    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);
    
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            ......
        }
        return changed;
    }
    

    ① setFrame(l, t, r, b) 可以理解为给mLeft 、mTop、mRight、mBottom赋值,然后基本就能确定View自己在父视图的位置了,这几个值构成的矩形区域就是该View显示的位置,这里的具体位置都是相对与父视图的位置。
    ② 返回值:如果新的尺寸和位置和之前的不同,返回true。

    3.2.1.4 View.onLayout()
    //============View.java============
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
    
    //=============ViewGroup.java===========
    @Override
    protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
    

    由于layout时已经将布局信息通过setFrame方法进行保存起来,在onLayout方法已经无须做额外事项,因此方法对于叶子view意义不大。但是对于容器类来说,需要一种遍历所有子view的机制,所以ViewGroup的子类需要重写此方法。

    3.2.2 DecorView布局过程

    3.2.2.1 DecorView.onLayout()
    //==============DecorView.java================
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         ......
    }
    

    DecorView的onLayout方法会调用父类FrameLayout的onLayout方法。

    3.2.2.2 FrameLayout.onLayout()
    //================FrameLayout.java==============
    @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 LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
                 final int width = child.getMeasuredWidth();
                 final int height = child.getMeasuredHeight();
                 ......
                 child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
    

    ① 对每一个child调用layout方法的,如果该child仍然是父布局,会继续递归下去;如果是叶子view,则会走到view的onLayout空方法,该叶子view布局流程走完。
    ② width和height分别来源于measure阶段存储的测量值,如果这里通过其它渠道赋给width和height值,那么measure阶段就不需要了。

    3.2.2.3 DecorView的布局流程图
    -.png

    3.3 draw过程分析

    3.3.1 View绘制流程

    3.3.1.1 ViewRootImpl.performDraw()
    //=== ViewRootImpl.java ===
    private void performDraw() {
        ......
        boolean canUseAsync = draw(fullRedrawNeeded);
        ......
    }
    
    private boolean draw(boolean fullRedrawNeeded) {
        ......
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) {
            return false;
        }
        ......
    }
    
    private boolean drawSoftware(...){
        ......
        mView.draw(canvas);
        ......
    }
    

    mView就是DecorView,这样就开始了DecorView视图树的draw流程了。

    3.3.1.2 DecorView.draw()
    //================DecorView.java==============
    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
    
        if (mMenuBackground != null) {
            mMenuBackground.draw(canvas);
        }
    }
    

    用完super.draw后,还画了菜单背景,由于FrameLayout和ViewGroup都没有重写该方法,所以就直接进入都了View类中的draw方法了。

    3.3.1.3 View.draw()
    public void draw(Canvas canvas) {
        ...
        /*
         * 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
        ...
        background.draw(canvas);
        ...
        // skip step 2 & 5 if possible (common case)
        ...
        // Step 2, save the canvas' layers
        ...
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
    
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ...
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
    
        // Step 4, draw the children
        dispatchDraw(canvas);
    
        // Step 5, draw the fade effect and restore layers
    
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ...
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }
    

    从代码上看,这里做了很多工作,咱们简单说明一下,有助于理解这个“画”工作。

    1. 第1步:背景绘制
      对应我我们在xml布局文件中设置的“android:background”属性,这是整个“画”过程的第一步,这一步是不重点,知道这里干了什么就行。

    2. 第3步,对View的内容进行绘制
      onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View.java 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。

    3. 第4步:对当前View的所有子View进行绘制
      dispatchDraw(canvas) 方法是用来绘制子View的,View.java 的dispatchDraw()方法是一个空方法,因为View没有子View,不需要实现dispatchDraw ()方法,ViewGroup就不一样了,它实现了dispatchDraw ()方法。

    4. 第6步:画装饰。
      这里指画滚动条和前景,其实平时的每一个view都有滚动条,只是没有显示而已。同样这也不是重点,知道做了这些事就行。

    3.3.1.4 View.onDraw()
    //=== View.java ===
    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }
    

    实现该方法来做“画”工作。也就是说,具体的view需要重写该方法,来画自己想展示的东西,如文字,线条等。

    3.3.1.4 ViewGroup.dispathcDraw()

    View.dispatchDraw()

    //=== View.java ===
    /**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {
    }
    

    view没有子视图,不需要进行绘制派发。

    ViewGroup.dispatchDraw()

    //=== ViewGroup.java ===
    @Override
    protected void dispatchDraw(Canvas canvas) {
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ......
        for (int i = 0; i < childrenCount; i++) {
            more |= drawChild(canvas, child, drawingTime);
            ......
        }
        ...... 
    }
    

    平时常用的LinearLayout、FrameLayout、RelativeLayout等常用的布局控件,都没有再重写该方法,DecorView中也一样,而是只在ViewGroup中实现了dispatchDraw方法的重写。所以当DecorView执行完onDraw方法后,流程就会切到ViewGroup中的dispatchDraw方法了。

    View.drawChild()

    //=== ViewGroup.java ===
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    

    画当前ViewGroup中的某一个子view,其中参数drawingTime表示“画”动作发生的时间点。

    View.draw(...)

    //=== View.java ===
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
      ......
      draw(canvas);
      ......
    }
    
    3.3.1.5 draw的绘制流程
    draw的递归流程.png

    3.3.2 DecorView绘制流程图

    ---.png

    3.4 绘制过程小结

    到目前为止,View的绘制流程就介绍完了。根节点是DecorView,整个View体系就是一棵以DecorView为根的View树,依次通过遍历来完成measure、layout和draw过程。而如果要自定义view,一般都是通过重写onMeasure(),onLayout(),onDraw()来完成要自定义的部分,整个绘制流程也基本上是围绕着这几个核心的地方来展开的。整个绘制过程流程示意图如下:


    绘制过程流程示意图.png

    参考链接

    [1] View绘制流程
    [2] Android View的绘制流程
    [3] Android图形系统(三)-View绘制流程
    [4] Android View绘制的三大流程
    [5] Android-View绘制流程浅析
    [6] Android View绘制流程
    [7] Android View 绘制流程(Draw)全面解析
    [8] View的绘制-draw流程详解
    [9] 深入理解 Android 之 View 的绘制流程

    相关文章

      网友评论

          本文标题:View:绘制流程

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