一直以来只是粗略的知道 View 的绘制会经过 measure,layout 到最终的 draw 三个过程, 但对其中详细的 measure 和 layout 过程一无所知, 很影响对一些特殊场景下的布局.
ViewRoot 和 DecorView
ViewRoot
ViewRoot 对应 ViewRootImpl 类, 它是连接 WindowManager 和 DecorView 的纽带, View 的三大流程均是通过 ViewRootImpl 来完成的. 在 ActivityThread 中, 当 Activity 对象被创建完毕后, 会将 DecorView 添加到 Window 中, 同时会创建 ViewRootImpl 对象, 并将 ViewRootImpl 对象和 DecorView 建立关联, 源码如下:
- root = new ViewRootImpl(view.getContext(),display);
- root.setView(view,wparams,panelParentView);
复制代码
View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的, performTraversals 会依次调用 performMeasure,performLayout 和 performDraw 三个方法, 这三个方法分别完成顶级 View 的 measure,layout 和 draw 三大流程. performMeasure 会调用 measure 方法, 在 measure 中又会调用 onMeasure 方法, 在 onMeasure 中则会对所有子元素进行 measure 过程, 这时候 measure 流程就从父容器传递到子元素中了, 这样就完成了一次 measure 过程. 接着子元素会重复父容器的 measure 过程, 如此返回就完成了整个 View 树的遍历. layout 和 draw 同上.
DecorView
DecorView 作为顶级 View, 一般情况下它内部会包含一个竖直方向的 LinearLayout,LinearLayout 中又包含上下两部分, 上面是标题栏, 下面是内容. 在 Activity 中我们通过 setContentView 所设置的布局文件其实就是被加到内容中的. DecorView 其实是一个 FrameLayout,View 层的事件都是先经过 DecorView, 然后才传递给我们的 VIew.
Measure
Measure 过程如上所述, 会由 ViewRootImpl 发起, 从顶层 DecorView 一层一层传递到最下层. View 的宽高会受到父容器限制的影响, 而父容器会在调用子 View 的 onMeasure 方法时把对子 View 宽高的限制传递过去. 要了解这种限制规则, 首先要了解一个类: MeasureSpec.
MeasureSpec
在测量过程中, 系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec, 然后在根据 MeasureSpec 来测量出 View 的宽高.
MeasureSpec 代表一个 32 位的 int 值, 高 2 位代表 SpecMode(测量模式), 低 30 位代表 SpecSize(规格大小), 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配, 为了方便操作, 也提供了打包和解包的方法.
SpecMode
UNSPECIFIED 父容器不对 VIew 有任何限制, 要多大给多大, 一般用于系统内部, 表示一种测量的状态.
EXACTLY 父容器已经检测出 View 所需的精确大小, 这个时候 View 的最终大小就是 SpecSize 所指定的值. 对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式.
AT_MOST 父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值, 具体多少要看 View 的具体实现. 对应 LayoutParams 中的 wrap_content.
DecorView 的测量
DecorView 作为顶级 View, 和普通 View 的测量有所不同, 其 MeasureSpec 由窗口尺寸和其自身的 LayoutParams 来决定, 并遵守以下规则:
LayoutParams.MATCH_PARENT: 精确模式, 大小就是窗口大小;
LayoutParams.WRAP_CONTENT: 最大模式, 大小不定, 但最大不能超过窗口大小;
固定大小: 精确模式: 大小为 LayoutParams 中指定的大小.
普通 View 的测量
对于普通的 View, 这里指我们 xml 布局中的 View,View 的 measure 过程由 ViewGroup 传递而来, 先看一下 ViewGroup 中测量子 View 的 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. 很显然子元素的 MeasureSpec 的创建与父容器的 MeasureSpec 和子元素本身的 LayoutParams 有关, 具体看一下 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
- 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);
- }
复制代码
上述方法不难理解, 它主要作用是根据父容器的 MeasureSpec 同时结合 View 本身的 LayoutParams 来确定子元素的 MeasureSpec, 参数中的 padding 是指父容器中已占用的空间大小, 因此子元素的可用大小为父容器的尺寸减去 padding.
childLayoutParams\parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY/childSize | EXACTLY/childSize | EXACTLY/childSize |
match_parent | EXACTLY/parentSize | AT_MOST/parentSize | UNSPECIFIED/0 |
wrap_content | AT_MOST/parentSize | AT_MOST/parentSize | UNSPECIFIED/0 |
在 ViewGroup 中计算好父容器期望子 View 的大小后, 此时就调用了子 View 的 onMeasure 方法, 可以看到 onMeasure 的默认实现中只是判断了是否有设置背景或最小尺寸限制, 如果有, 则在无限制模式下将尺寸替换为最小尺寸. 这里也能看出 minHeight minWidth 不是在任何控件中都管用. 需要注意的是 onMeasure 方法没有返回值, 需要调用
setMeasuredDimension()
来保存我们的测量结果. 获取测量结果同理,
- getMeasuredDimension()
- .
ViewGroup 并没有 onMeasure 的实现, 一般都由具体的实现类根据自己的业务来实现 onMeasure 方法, 比如 LinearLayout.
Measure 流程总结
通过自身的 MeasureSpec 和子 view 的 LayuoutParams, 生成子 view 的 MeasureSpec. 这一步调用的是 getChildMeasureSpec(int spec, int padding, int childDimension)方法.
调用子 view 的 measure(int widthMeasureSpec, int heightMeasureSpec)方法, 来测量子 view 的宽高.
在子 view 测量结束之后, 根据情况来计算自身的宽高. 假如自己的 MeasureSpec 是 Exactly 的, 那么可以直接将 SpecSize 中的大小作为自己的宽或高; 如果是 wrap_content 或者其他的, 那么就需要在每一个子 view 测量完之后, 调用子 view 的 getMeasuredHeight()和 getMeasuredWidth()来获得子 view 测量的结果, 然后根据情况计算自己的宽高.
使用 setMeasuredDimension(int measuredWidth, int measuredHeight)方法保存测量的结果.
自定义 Measure
我们如果要自定义 View 的 onMeasure 过程的话一般有两种方式:
修改测量结果 直接重写 onMeasure 方法, 在 super.onMeasure 之后修改我们期望的尺寸并保存就好;
自定义测量过程 不调用 super.onMeasure, 直接计算我们期望的尺寸, 并调用 resolveSize()来让计算出的尺寸符合父容器的要求, 最后别忘了保存.
Layout
Layout 过程相对于 Measure 来说就比较简单了, 同样只需要关注 Layout()和 onLayout()方法即可, 可以简单理解为在 layout 方法中确定自己的位置, onLayout 方法中确定所有子元素的位置. View 和 ViewGroup 中都有 layout(), 但都没有实现 onLayout(), 因为不同类型的布局对 onLayout()的要求是不一样的.
当我们自定义了一个 ViewGroup 的时候, 会先确定这个 ViewGroup 的位置, 然后, 通过重写 onLayout() 方法, 遍历所有的子元素并调用其 layout() 方法, 在 layout()方法中 onLayout()方法又会被调用. ViewGroup 就是通过这个过程, 递归地对所有子 View 进行了布局. 来看一下 View 类中的 layout()方法的源码:
- /**
- * 本方法用来给一个 View 和它的所有子 View 设置尺寸和位置;
- * 这是 Android 布局机制的第二个阶段(第一个阶段是测量);
- * 在这个阶段中, 每个父容器都调用 layout()方法来定位它的子 View;
- * 子类不能重写这个方法, 而应该重写 onLayout()方法;
- * 在 onLayout()方法中调用 layout()方法来设置每个子 View 的位置.
- *
- * @param l 相对于父容器左边的距离
- * @param t 相对于父容器上边的距离
- * @param r 相对于父容器右边的距离
- * @param b 相对于父容器下边的距离
- */
- @SuppressWarnings({"unchecked"})
- public void layout(int l, int t, int r, int b) {
- if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
- onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
- mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- }
- int oldL = mLeft;
- int oldT = mTop;
- int oldB = mBottom;
- int oldR = mRight;
- 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);
- if (shouldDrawRoundScrollbar()) {
- if(mRoundScrollbarRenderer == null) {
- mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
- }
- } else {
- mRoundScrollbarRenderer = null;
- }
- mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
- ListenerInfo li = mListenerInfo;
- if (li != null && li.mOnLayoutChangeListeners != null) {
- ArrayList<View.OnLayoutChangeListener> listenersCopy =
- (ArrayList<View.OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
- int numListeners = listenersCopy.size();
- for (int i = 0; i < numListeners; ++i) {
- listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
- }
- }
- }
- mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
- mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
- }
复制代码
从源码中可以看出这个方法的大致流程: 首先通过 setFrame()方法来设置 View 的四个位置元素的位置, 即初始化 mLeft,mTop,mRight 和 mBottom 这四个值. View 的四个顶点一旦确定, 那么 View 在父容器中的位置也就确定了; 接着会调用 onLayout() 方法, 这个方法的用途是父容器确定子元素的位置.
在自定义布局的时候, 我们的任务就是: 遍历所有的子元素, 确定它们的大小和位置 (大小主要是通过 getMeasuredWidth() 和 getMeasuredHeight() 两个方法, 取出在 onMeasure() 方法中测量得到的宽 / 高; 位置需要自行设置), 然后调用 view.layout() 方法或直接调用 ViewGroup 中的方法 setChildFrame() 方法(setChildFrame() 方法内部调用的就是 view.layout()方法), 将子元素布局到这个 ViewGroup 中.
最后还需要说明一点,"测量宽 / 高" 和 "最终宽 / 高" 是两个不同的概念. 测量宽 / 高是在 onMeasure()方法中测量得到的宽度或高度, 而最终宽 / 高是在 onLayout()方法中最终放置的子元素的宽度或高度. 在 View 的默认实现中, View 的测量宽 / 高和最终宽 / 高是相等的, 但是测量宽 / 高的赋值时机较早.
Layout 总结
父 layout 在自己的 onLayout()函数中负责对子 view 进行布局, 安排子 view 的位置, 并且将测量好的位置 (上下左右位置) 传给子 view 的 layout()函数.
子 view 在自己的 layout()函数中使用 setFrame()函数将位置应用到视图上, 并且将新位置和旧位置比较来得出自己的位置和大小是否发生了变化 (changed), 之后再调用 onLayout() 回调函数.
如果此时子 view 中还有其他 view, 那么就在自己的 onLayout()函数中对自己的子 view 进行第 1 补的布局操作, 如此循环, 只到最后的子 view 中没有其他 view, 这样就完成了所有 view 的布局.
来源: https://juejin.im/post/5b56d77cf265da0fa50a1ba1