欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec

程序员文章站 2022-07-14 11:34:02
...

要点

View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec

一、初识RootView、DecorView

为了更好的熟悉view的三大流程(测量、摆放、绘制)我们就先普及下RootView、DecorView基本概念。

1、ViewRoot

1、ViewRoot 对应ViewRootImlp 类,它其实是连接 WindowManger 和 DecorView 的桥梁。View 的三大流程都是通过 ViewRoot 来完成的。
2、在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象与 DecorView 进行关联。
3、整个 View 的绘制流程,是从 ViewRoot 的 performTraversals 方法开始的。 它经过measure、layout、draw 这三个过程才最终将一个 View 绘制出来,其中 measure 是用于测量 View 的宽高的,layout 是用于当继承 ViewGroup 时确定子 View 位置的, draw 则是负责将 View 绘制在屏幕上。如下图(performTraversals 工作流程图)
4、performTraversals 会依次调用 performMeasure、performLayout、performDraw 这三个方法,这三个方法会依次完成* View 的 measure、layout、draw 三大流程。
5、其中 performMeasure 方法会接着调用 measure 方法,在 measure 方法中又会去调用 onMeasure 方法。在 onMeasure 方法中对所有的子元素进行了 measure 过程 ,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着,子元素又重复了一遍 measure 过程,如此反复,直到没有了子元素。这样就完成了整个 View 树的遍历。
6、其中 layout 和 draw 的流程同上,两者同样都有 performLayout 、performDraw 和 layout、draw 方法,唯一不同的是,performDraw 的传递是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有什么本质上的区别。

View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec

2、三大流程简介
  • measure
    这个过程决定了 View 的宽/高。measure 完成以后,可以通过 getMeasureWidth 和 getMeasureHeight 来获取View 测量后的宽/高。

  • layout
    这个过程决定了 View 四个定点的坐标和实际 View 的宽/高。完成以后,可以通过 getTop、getLeft、getBottom、getRight 来获取 View 的四个顶点的位置,并可以通过 getWidth 和 getHeight 方法获取 View 的最终宽/高。
    ps:其实getTop、getBottom、getRight、getLeft这四个方法是view的方法在view基础知识中有讲解参考:View的事件体系(一)view基础和view的几种滑动方式中的图解

  • draw
    这个过程决定了 View 的显示,只有当 draw 方法完成以后,View 才能够显示到屏幕上。

3、DecorView

View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec

如上图(图片来源网络):
1、DecorView 在一般情况下,内部会包含一个竖直的 LInearLayout,里面有上下两部分(具体情况和安卓版本和主题有关),上面是标题栏,下面是内容,内容布局有一个默认的 id: content。
2、DecorView 继承自 FrameLayout,是一个 ViewGroup。在整个 ViewTree 中, DecorView 是整个 ViewTree 的顶层 View。View 的所有事件,都先经过 DecorView,然后再传递给 View
我们在 Activity 中 通过 setContentView() 方法设置的布局,其实就是添加到了内容部分里。如果我们想获取到内容布局的话,可以通过如下方法获取:

ViewGroup content = (ViewGroup) findViewById(android.R.id.content);

View childAt = content.getChildAt(0);

二、理解MeasureSpec

MeasureSpec 是 View 的一个内部类,代表了一个32位的 int 值。前 2 位代表 SpecMode,后 30 位代表 SpecSize。SpecMode 是指测量模式,SpecSize 是指在某种测量模式下的规格大小。

测量模式(SpecMode )有3种:UNSPECIFIED,EXACTLY,AT_MOST。我们只需要使用2个bit就可以做到,因为2个bit取值范围是[0,3]里面可以存放4个数足够我们用了。那么Google是怎么把一个int同时放测量模式和尺寸信息呢?我们知道int型数据占用32个bit,而google实现的是,将int数据的前面2个bit用于区分不同的布局模式,后面30个bit存放的是尺寸的数据。

该类在很大程度上决定了 View 的尺寸规格,除此之外父容器也影响 View 的 MeasureSpec 的创建过程。系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 来测量出 View 的宽高。需要注意的是,这个的宽高是测量的宽高,并不一定是 View 最终的宽高。后面会详细了解 MeasureSpec 的创建过程,在了解之前,首先来了解下 MeasureSpec 的三种测量模式。
参考下图理解:
布局参数参考:
从LayoutParams说起到代码动态布局
关于LayoutParams的使用

View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec

1、三种测量模式

SpecMode 和 SpecSize 组成了 MeasureSpec,MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象创建,并提供了对应的打包、解包方法
首先我们看看MeasureSpec源码。

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        
         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;

// 打包操作
 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);
            }
        }
}

// 解包  获得 mode
 public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
   //解包 获得size     
   public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
2、SpecMode 三类
  • UNSPECIFIED:
    父容器不对子元素作任何约束,子 View 想要多大就给多大。这种情况一般用在系统内部,表示一种测量的状态。一般情况下,我们不用关注该测量模式。

  • EXACTLY:
    精准模式。父容器已经解决了子元素所需要的精准大小,这时候子 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值。

  • AT_MOST:
    最大模式。父最大模式容器指定了一个可用大小即 SpecSize,子元素最大不可以超过指定的这个值。它对应于LayoutParams 中的 wrap_content。

3、MeasureSpec和LayoutParams 的关系

我们知道:对于普通 View,MeasureSpec 是由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定的。
看看源码,MeasureSpec 是怎么被创建出来的,由于普通 View 的 measure 过程由 ViewGroup 传递而来,所以首先来看一下 ViewGroup 的:measureChildWithMargins()方法

         
 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
            //获取子元素的布局参数
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        // 根据父容器WidthMeasureSpec,子view自身布局参数 ,子view的margain、padding来获得 子view的WidthMeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
           // 根据父容器WidthMeasureSpec,子view自身布局参数 ,子view的margain、padding来获得 子view的WidthMeasureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
       //开始测量子view
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

布局参数参考:https://blog.csdn.net/u012810020/article/details/51970771
首先获取到了子元素的 LayoutParams 然后根据当前子元素的 LayoutParams 和父类的MeasureSpec,获取了子元素的 MeasureSpec,最后调用了子元素的 measure。
继续深入getChildMeasureSpec源码如下:

//参数 父容器MeasureSpec,父容器padding,子view大小
  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);//父容器mode
        int specSize = MeasureSpec.getSize(spec);//父容器size

        int size = Math.max(0, specSize - padding); //父容器可用size空间==父容器总空间-padding空间

        int resultSize = 0;//测量出的子元素size 先默认为0;
        int resultMode = 0;//测量出的子元素mode先默认为0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:// 父控件EXACTLY模式时
            if (childDimension >= 0) {//子元素采用固定数值时(比如xml中宽高设置了具体dp)
                resultSize = childDimension; // 测量出 子元素size 为childDimension
                resultMode = MeasureSpec.EXACTLY;// 测量出 子元素模式为EXACTLY
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//子元素设置了MATCH_PARENT时
                // Child wants to be our size. So be it.
                resultSize = size;// 子元素大小为 size,这个size是父容器大小减去padding的空间
                resultMode = MeasureSpec.EXACTLY;//子元素模式为EXACTLY
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//子元素设置为WRAP_CONTENT时
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size; // 子元素大小为 size,这个size是父容器大小减去padding的空间
                resultMode = MeasureSpec.AT_MOST;//子元素模式为AT_MOST
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST://当父容器为AT_MOST模式时
            if (childDimension >= 0) {//子元素设置了固定值时(比如宽高设置:50dp)
                // Child wants a specific size... so be it
                resultSize = childDimension; //大小为子元素大小
                resultMode = MeasureSpec.EXACTLY;//模式为EXACTLY
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//子元素设置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;//模式AT_MOST
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//子元素设置WRAP_CONTENT时
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;//大小为父容器剩余空间(参考上文size解释)
                resultMode = MeasureSpec.AT_MOST;//模式AT_MOST
            }
            //(可以看出自定义view时父容器的AT_MOST我们不处理时view设置match_parent、wrap_content都是match_parent效果)
            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);
    }
getChildMeasureSpec展示了普通view的MeasureSpec创建规则,根据源码总结出下表:

View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec

总结:(详细的分析请看注释,我已经几乎都注释了
1、当 View 采用固定宽高(确切值)的时候,不管父容器的 MeasureSpec 是什么,View 的测量模式都是 EXACTLY 也就是精准模式。并且其大小遵循 LayoutParams 中的大小。
2、当 View 的宽高是 match_parent 的时候,如果父控件是精准模式,那么 View 也是精准模式,并且大小是父容器的剩余空间。如果父控件是最大模式,那么 View 也是最大模式,并且大小不会超过父容器的剩余空间。
3、当 View 的宽高是 wrap_content 的时候,不管父控件是精准模式还是最大模式,View 的模式最是最大模式,并且大小不能够超过父容器的剩余空间。
ps:UNSPECIFIED 系统使用,我们不考虑。

实际开发中:

实际开发中:
1、UNSPECIFIED我们不常见。几乎不用处理。
2、EXACTLY我们也几乎不用处理,
3、AT_MOST 这种我们要处理。(上面源码中AT_MOST状况下的注释如下)
(可以看出自定义view时父容器的AT_MOST我们不处理时view设置match_parent、wrap_content都是match_parent效果)
栗子如下:

首先自定义view继承view啥也不处理:

/**
 * Create by SunnyDay on 2019/04/12
 */
public class MyLayout extends View {
    public MyLayout(Context context) {
        super(context);
    }

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
   
}

直接使用:
View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec
结果:
View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec

如上:
自定义view时,除了第一个view的高度我们设置精确值时显示无误,我们设置为wrap_content时的效果就是match_parent的效果。
ps:中间写了个TextView以做隔离区分
如何处理参考view的三大流程

三、小结

总结了一篇,发现MeasureSpec还真不好理解。。。。。
参考文章:https://blog.csdn.net/Airsaid/article/details/53576087

The end

本文来自<安卓开发艺术探索>笔记总结

View的工作原理(二)Measure过程