参考书籍:《Android开发艺术探索》 任玉刚
如有错漏,请批评指出!
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);
关于Android控件架构的内容前面讲过 Android 控件架构与自定义控件,这里不再赘述。
View的绘制流程是从ViewRoot的 performTraversals 方法开始的,它经过 measure、layout、draw三个过程才能最终将一个View绘制出来。其中 measure 用来测量View的宽高,layout 用来确定View在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。具体可看下图:
从图中可以看到,整个页面的绘制是层层进行的,performMeasure 方法会调用 measure 方法,measure 方法中会调用 onMeasure 方法,在 onMeasure 方法中会对所有的子View进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,接着子 View会重复 和父容器相同的 measure 过程,这样反复之后就完成了整个 View 树的遍历。performLayout 和 performDraw 的传递流程也是如此,唯一不同的就是 performDraw 的传递过程是在 draw 方法中调用 dispatchDraw 方法从而对子View进行绘制。
- measure 过程决定了View的宽高,measure完成以后,可以通过 getMeasureWidth 和 getMeasureHeight 方法来获取到View测量后的宽 / 高,在大多情况下,它都等于View最终的宽 / 高(存在特殊情况)。
- layout 过程决定了View 的四个顶点坐标和实际的View宽高,完成之后,可以通过 getTop、getButtom、getLeft 和 getRight来拿到四个位置参数,并可以通过getWidth 和 getHeight方法拿到View的最终宽高。
- draw过程决定了View的显示,只有draw方法完成后,View的内容才能呈现在屏幕上。
关于 MeasureSpec
为了更好地理解 View 的测量过程,首先得理解 MeasureSpec 是个什么概念,从字面看,就是“测量规格”,在前面的博客中也简单的介绍过 MeasureSpec 是什么,下面从源码的角度具体进行分析。
-
MeasureSpec 是一个32位的int值,它的高2位用来表示SpecMode,即测量模式,低30位用来表示SpecSize,即规格大小。下面来看一下MeasureSpec类中主要方法及常量的定义:
public static class MeasureSpec { // 移位 private static final int MODE_SHIFT = 30; // 3的16进制表示 左移30位 private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 0 左移30位 表示 UNSPECIFIED 测量模式 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 1左移30位 表示 EXACTLY 测量模式 public static final int EXACTLY = 1 << MODE_SHIFT; // 2左移30位 表示 AT_MOST 测量模式 public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } }
Measurespec通过将SpecMode 和 SpecSize打包成一个 int 值来避免过多的对象内存分配,并且提供了相应的打包和解包方法。 SpecSize 和 SpecMode其实都是int值,通过二进制位的方式记录为一个 MeasureSpec 值。
SpecMode有三种,分别如下:
- UNSPECIFIED
父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。 - EXACTLY
父容器已经测量出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的 match_parent 和 具体的数值这两种情况。
3.AT_MOST
父容器指定了一个可用大小,View的SpecSize不能大于这个值,具体是多大要看不同View自身的实现。它对应于LayoutParams中的 wrap_content。
- UNSPECIFIED
-
MeasureSpec 和 LayoutParams对应关系
我们需要知道,MeasureSpec并不是唯一由LayoutParams 决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,即在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量View的宽高。这一点可以通过阅读源码来验证,下面来进行分析。首先当然要从顶级View(即DecorView)着手,对于DecorView,它的MeasureSpec由窗口的尺寸和其自身的Layoutparams来共同确定。
在ViewRootimpl类中的 measureHierarchy 方法中有一段代码:
int childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
这段代码展示了DecorView的MeasureSpec的创建过程,其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸。
下面再来看 getRootMeasureSpec 方法的实现:
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; }
通过这段代码,可以根据DecorView的LayoutParams中的宽高参数来划分:
- LayoutParams.MATCH_PARENT:DecorView的宽高就是屏幕的宽高,SpecMode为精确模式(EXACTLY)。
- LayoutParams.WRAP_CONTENT:DecorView大小不定,但是不超过屏幕尺寸,SepcMode最大模式(AT_MOST)。
- 固定值:DecorView的宽高为LayoutParams中指定的宽高,SpecMode为精确模式(EXACTLY)。
而对于普通View,它的Measurespec由其父容器的MeasureSpec和自身的LayoutParams共同决定。View的measure过程由父ViewGroup传递而来,因此,我们先来看ViewGroup的 measureChildWithMargins 方法:
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); }
这个方法会对子元素进行 measure,在调用子元素的measure方法之前,会通过getChildMeasureSpec 方法来得到子元素的MeasureSpec。从getChildMeasureSpec 方法的参数来看,子元素的MeasureSpec的创建与父容器的MeasureSpec 、padding值以及子元素本身的LayoutParams和margin值有关(想想外边距内边距,很好理解)。
接下来,看一下ViewGroup的 getChildMeasureSpec 方法:
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 // LayoutParams.MATCH_PARENT = -1 // LayoutParams.WRAP_CONTENT = -2 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; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
这段代码比较长,需要仔细推敲,不过逻辑还是很好理解的,要注意这个方法传入的参数 padding 实际上是父容器的padding值与View自身的margin值的和。因此,当子View的测量模式为AT_MOST时,这个最大值,也就是子View的最大可用空间是需要用父容器的MeasureSpec减去这个padding的,即:
将这段代码的逻辑理清,其实就是下面这张表中的内容:int size = Math.max(0, specSize - padding);
表中的parentSize是指子View的最大可用空间(其实就是上面说的size)。总结一下,其实就是4条规则:
- 当View指定固定值作为宽高时,不管父容器的MeasureSpec为什么模式,View的SpecMode精确模式,并且都是宽高是指定值;
- 当View的宽高是match_parent时,如果父容器的SpecMode是精确模式,View的SpecMode也是精确模式,并且其宽高是父容器的可用空间,如果父容器是最大模式,那么View的SpecMode也是最大模式,并且宽高不超过父容器的可用空间;
- 当子View的宽高是wrap_content时,不管父容器的SpecMode精确模式还是最大模式,View的SpecMode都是最大模式,且其宽高不会超过父容器可用空间。
- 对于UNSPECIFIED模式,主要是系统内部会用到,我们暂时不需要关注。
上一篇:Android学习笔记(四)| Android 控件架构与自定义控件
下一篇:Android学习笔记(六)| View的工作原理(下)