Android——View的工作原理(一)

主要介绍内容:

  • ViewRoot 和 DecorView
  • MeasureSpec
    • MeasureSpec
    • MeasureSpec 和 LayoutParams 的对应关系
  • View的工作流程
    • measure 过程
    • layout 过程
    • drow 过程
  • 自定义View
    • 自定义 View 的分类
    • 自定义 View 条件条件
    • 自定义 View 示例
    • 自定义 View 的思想

ViewRoot 和 DecorView

在正式介绍 View 的三大流程之前,我们必须先介绍一些基本概念,这样才能更好地理解 View 的 measure、layout 和 draw 过程,这里主要先介绍 ViewRoot 和 DecorView 的概念。

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联,这个过程可参看如下源码:

root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);

View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽 和 高,layout 用来确定 View 在父容器中放置的位置,而 draw 则则负责将 View 绘制在屏幕上。针对 performTraversals 的大致流程,可用流程图来表示:

这里写图片描述

如上图所示,performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有子元素进行 measure 过程,这个时候 measure 流程就从父元素传递到了子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父元素的 measure 过程,如此反复就完成了这个 View 树的遍历。同理,performLayout 和 performDraw 的传递流程 和 performMeasure 是类似的,唯一不同的是,performDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有本质区别。

measure 过程决定了 View 的宽/高,Measure 完成以后,可以通过 getMeasuredWidth 和 getMeasuredHeight 方法来获取到 View 测量后的宽/高,在几乎所有情况下它都等同于 View 最终的宽/高,但是特殊情况除外,这点在后面会进行说明。 layout 过程决定了 View 的四个顶点的坐标 和 实际的 View 的宽/高,完成以后,可以通过 getTop、getBottom、getLeft 和 getRight 来拿到 View 的四个顶点的位置,并可以通过 getWidth 和 getHeight 方法来拿到 View 的最终宽/高。 draw 过程则决定了 View 的显示,只有 draw 方法完成以后 View 的内容才能呈现在屏幕上。

DecorView 作为顶级 View,一般情况下它内部都会包含一个竖直方向的 LinearLayout,在这个 LinearLayout 里面有上下两个部分(具体情况 和 Android 版本及主题有关),上面是标题栏,下面是内容栏。在 Activity 中我们通过 setContentView 所设置的布局文件其实就是被加入到内容栏之中的,而内容栏的 id 是 content,因此可以理解为 Activity 指定布局的方法不叫 setView 而叫 setCOntentView,因为我们的布局的确加到了 id 为 content 的 FrameLayout 中。如何得到 content 呢? 可以这样: ViewGroup content = (ViewGroup) findViewById(android.R.id.content)。 如何得到我们设置的 View 呢? 可以这样:content.getChildAt(0); 。 同时,通过源码我们可以知道,DecorView 其实是一个 FrameLayout,View 层的事件都先经过 DecorView,然后才传递给我们的 View。 如图所示:

这里写图片描述

对应 DecorView 不太了解的可以根据一下博客自行了解:

好了,关于 ViewRoot 以及 DecorView 我们就介绍到这里,下面开始我们的 MeasureSpec。

MeasureSpec

为了更好地理解 View 的测量过程,我们还需要理解 MeasureSpec。从名字上来看,MeasureSpec 看起来像 “测量规格” 或者 “测量说明书”,不管怎么翻译,它看起来都好像或多或少地决定了 View 的测量过程。通过源码可以发现,MeasureSpec 的确参与了 View 的 measure 过程。这里大家可能会有这么一个疑问,MeasureSpec 是干什么的呢? 确切来说,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响 View 的 MeasureSpec 的创建过程。在测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后在根据这个 MeasureSpec 来测量出 View 的 宽/高。上面提到过,这里的宽/高是测量宽/高,不一定等于 View 的最终宽/高。MeasureSpec 看起来有点复杂,其实它的实现是很简单的,下面我们就来分析一下 MeasureSpec。

MeasureSpec

MeasureSpec代表一个 32 位 int 值,高 2 位代表 SpecMode, 低 30 位代表 SpecSize,SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格的大小。下面先看一下 MeasureSpec 内部的一些常量的定义,通过下面的代码,应该不难理解 MeasureSpec 的工作原理:

public class View implements ... {  
     ...  
     public static class MeasureSpec {  
        private static final int MODE_SHIFT = 30; //移位位数为30  
        //int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。  
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  

        //向右移位30位,其值为00 + (30位0)  , 即 0x0000(16进制表示)  
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        //向右移位30位,其值为01 + (30位0)  , 即0x1000(16进制表示)  
        public static final int EXACTLY     = 1 << MODE_SHIFT;  
        //向右移位30位,其值为02 + (30位0)  , 即0x2000(16进制表示)  
        public static final int AT_MOST     = 2 << MODE_SHIFT;  

        //创建一个整形值,其高两位代表mode类型,其余30位代表长或宽的实际值。可以是WRAP_CONTENT、MATCH_PARENT或具体大小exactly size  
        public static int makeMeasureSpec(int size, int mode) {  
            return size + mode;  
        }  
        //获取模式  ,与运算  
        public static int getMode(int measureSpec) {  
            return (measureSpec & MODE_MASK);  
        }  
        //获取长或宽的实际值 ,与运算  
        public static int getSize(int measureSpec) {  
            return (measureSpec & ~MODE_MASK);  
        }  

    }  
    ...  
} 

MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法。 SpecMode 和 SpecSize 也是一个 int 值,一组 SpecMode 和 SpecSize 可以打包为一个 MeasureSpec,而一个 MeasureSpec 可以通过解包的形式来得出其原始的 SpecMode 和 SpecSize,需要注意的是这里提到的 MeasureSpec 是指 MeasureSpec 所代表的 int 值,而并非 MeasureSpec 本身。

SpecMode 有三类,每一类都表示特殊的含义,如下所示:

UNSPECIFIED

父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。

EXACTLY

父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。相当于在 XML 布局文件中指定 android:layout_width=”match_parent”

AT_MOST

父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content ,相当于在 XML 布局文件中指定 android:layout_width=”wrap_content”

关于 MeasureSpec 相关就介绍到这里,下面开始我们的 “MeasureSpec 和 LayoutParams 的对应关系”,在介绍之前有兴趣的可以先去了解下 LayoutParams 在这里给出一篇博客:Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(上)

MeasureSpec 和 LayoutParams 的对应关系

上面提到,系统内部是通过 MeasureSpec 来进行 View 的测量,但是正常情况下我们用 View 指定 MeasureSpec,尽管如此,但是我们可以给 View 设置 LayoutParams。在 View 测量的时候,系统会将 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec,然后在根据这个 MeasureSpec 来确定 View 测量后的宽/高。需要注意的是,MeasureSpec 不是唯一有 LayoutParams 决定的, LayoutParams 需要和父容器一起才能决定 View 的 MeasureSpec,从而进一步决定 View 的宽/高。另外,对于顶级 View(即 DecorView)和普通 View 来说,MeasureSpec 的转换过程略有不同。对于 DecorView,其 MeasureSpec 由窗口的尺寸 和 其自身的 LayoutParams 来共同确定;对应普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定,MeasureSpec 一旦确定后, onMeasure 中就可以确定 View 的测量宽/高。

对于 DecorView 来说,在 ViewRootImpl 中的 measureHierarchy 方法中有如下一段代码,它展示了 DecorView 的 MeasureSpec 的创建过程,其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸;

childWidthMeasureSpec = getRootmeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接着再看一下 getRootMeasureSpec 方法的实现:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {  
    int measureSpec;  
    switch (rootDimension) {  

    case ViewGroup.LayoutParams.MATCH_PARENT:  
        // Window不能调整其大小,强制使根视图大小与Window一致  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
        break;  
    case ViewGroup.LayoutParams.WRAP_CONTENT:  
        // Window可以调整其大小,为根视图设置一个最大值  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
        break;  
    default:  
        // Window想要一个确定的尺寸,强制将根视图的尺寸作为其尺寸  
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
        break;  
    }  
    return measureSpec;  
}  

通过上述代码, DecorView 的 MeasureSpec 的产生过程就很明确了,具体来说其遵守如下规则,根据它的 LayoutParams 中的宽/高的参数来划分。

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
  • 固定大小(比如 100dp):精确模式,大小为 LayoutParams 中指定的大小。

对应普通 View 来说,这里是指我们布局中的 View,View 的 measure 过程由 ViewGroup 传递而来,先看一下 ViewGroup 的 measureChildWithMargins 方法:

/** 
 * Ask one of the children of this view to measure itself, taking into 
 * account both the MeasureSpec requirements for this view and its padding 
 * and margins. The child must have MarginLayoutParams The heavy lifting is 
 * done in getChildMeasureSpec. 
 */  
//基本流程同于measureChild()方法,但添加了对子View Margin的处理,即:android:margin属性或者android:marginLeft等属性的处理  
//widthUsed参数  表示该父View已经使用的宽度  
//heightUsed参数  表示该父View已经使用的高度  
protected void measureChildWithMargins(View child,  
        int parentWidthMeasureSpec, int widthUsed,  
        int parentHeightMeasureSpec, int heightUsed) {  
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  

    //获得子View的childWidthMeasureSpec和childHeightMeasureSpec值  
    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);  
}  

上述方法会对子元素进行 measure,在调用子元素的 measure 方法之前会先通过 getChildMeasureSpec 方法来得到子元素的 MeasureSpec。从代码来看,很显然,子元素的 MeasureSpec 的创建与父容器的 MeasureSpec 和 子元素本身的 LayoutParams 相关,此外还和 View 的 margin 及 padding 相关,具体情况可以看一下 ViewGroup 的 getChildMeasureSpec 方法,如下所示:

/** 
 * Does the hard part of measureChildren: figuring out the MeasureSpec to 
 * pass to a particular child. This method figures out the right MeasureSpec 
 * for one dimension (height or width) of one child view. 
 * 
 * The goal is to combine information from our MeasureSpec with the 
 * LayoutParams of the child to get the best possible results. 
 */  
// spec参数                                    表示该父View本身所占的widthMeasureSpec 或  heightMeasureSpec值  
// padding参数                          表示该父View的边距大小,见于android:padding属性 或android:paddingLeft等属性标记  
// childDimension参数  表示该子View内部LayoutParams属性的值,可以是wrap_content、match_parent、一个精确指(an exactly size),  
//           例如:由android:width指定等。  
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
    int specMode = MeasureSpec.getMode(spec);  //获得父View的mode  
    int specSize = MeasureSpec.getSize(spec);  //获得父View的实际值  

    int size = Math.max(0, specSize - padding); //父View为子View设定的大小,减去边距值,  

    int resultSize = 0;    //子View对应地 size 实际值 ,由下面的逻辑条件赋值  
    int resultMode = 0;    //子View对应地 mode 值 , 由下面的逻辑条件赋值  

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

上述方法不难理解,它的主要作用是根据父容器的 MeasureSpec 同时结合 View 本身的 LayoutParams 来确定子元素的 MeasureSpec,参数中的padding 是指 父容器中已占用的空间大小,因此子元素可用的大小为父容器的尺寸减去 padding,具体代码如下所示:

    int specSize = MeasureSpec.getSize(spec);  //获得父View的实际值  

    int size = Math.max(0, specSize - padding); //父View为子View设定的大小,减去边距值,  

getChildMeasureSpec 方法清楚展示了普通 View 的 MeasureSpec 的创建规则,为了更清晰理解 getChildMeasureSpec 的逻辑,这里提供一个表,表中对 getChildMeasureSpec 的工作原理进行了梳理,注意,表中的 partentSize 是指父容器中目前可使用的大小。

这里写图片描述

针对上表,前面已经提到,对于普通View,其MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定,那么针对不同的父容器和 View 本身不同的 LayoutParams,View 就可以有多种 MeasureSpec,这里简单说一下,当 View 采用固定宽/高的时候,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精准模式并且其大小遵循 LayoutParams 中的大小。当 View 的宽/高是 match_parent 时,如果父容器的模式是精准模式,那么 View 的 MeasureSpec 也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 的 MeasureSpec 也是最大模式并且其大小不会超过父容器的剩余空间。当 View 的宽/高是 warp_content 时,不管父容器的模式是精准还是最大化模式,View 的模式总是最大化并且其大小不能超过父容器的剩余空间。可能大家会发现,在我们的分析中漏掉了 UNSPECIFIED 模式,那是因为这个模式主要用于系统内部多次 Measure 的情形,一般来说,我们不需要关注此模式。

通过上表可以看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地确定出子元素的 MeasureSpec了。有了 MeasureSpec 就可以进一步确定出子元素测量后的大小了。需要说明的是,上表并非是什么经验总结,它只是 getChildMeasureSpec 这个方法以表格的形式呈现出来而已。

由于篇幅问题,本篇博客就暂时介绍到这里,在下一篇博客中我们会详细介绍 View的工作原理以及具体的 onMeasure、onLayout 和 onDraw 的调用过程。

相关博文推荐:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值