内容简介:Activity 是通过 Window 与 View系统进行交互,而 Window 则是通过 ViewRootImpl 与 根View(DecorView)交互,View 最关键的三个步骤就是测量(measure)、布局(layout)、绘制(draw), 最开始绘制的入口是 ViewRootImpl 类的 performTravesals 方法,下图对整体流程做了个概述:MeasureSpec: 这个关键对象贯穿在测量流程中,我们可以把它理解成一个 View 自身的「测量规格」, 它包含两个变量一个是 m
Activity 是通过 Window 与 View系统进行交互,而 Window 则是通过 ViewRootImpl 与 根View(DecorView)交互,View 最关键的三个步骤就是测量(measure)、布局(layout)、绘制(draw), 最开始绘制的入口是 ViewRootImpl 类的 performTravesals 方法,下图对整体流程做了个概述:
二、源码分析
1. measure
MeasureSpec: 这个关键对象贯穿在测量流程中,我们可以把它理解成一个 View 自身的「测量规格」, 它包含两个变量一个是 mode(测量模式),另一个是 size(测量尺寸)。
我觉得源码有一点设计的特别巧妙,但也很难理解,那就是用位操作来表示某个状态值。这么做的原因是能节省更多的内存以及计算更快。MeasureSpec 是一个数据结构,但是它主要是用来制作一个 int 整型的变量,这个变量高 2 位表示测量模式,低 30 位表示测量尺寸,这是根据模式的数量决定的,总共就三种模式,因此用两位就很够了,如 01 000000000000000000001111010101 粗体即表示模式。两个变量合并成一个变量了,看到这种方式简直就像发现新大陆一般。。但不推荐自己写代码的时候用这种方式,因为别人不一定看得懂,可读性差。。
三种模式:
- UNSPECIFIED: 父视图不强加任何约束给子视图,子视图想多大就多大,此模式一般不会用到,以下讨论就略过这个模式了。
- EXACTLY: 精确模式,父视图已经知道子视图确切的尺寸,一般对应 match_parent 和 具体数值。
- AT_MOST: 最大模式,在父视图允许的范围内,子视图尽量的大,一般对应 wrap_content。
LayoutParams: 布局参数。每个 View 都有自身的布局参数,最最基础的就是宽高,我们平时最常见的就是设置width 和 height 为 match_parent 或 wrap_content。然后不同的 LayoutParams 有不同的属性,如 LinearLayout.LayoutParams 就增加了 margin 相关的属性。
View 自身的 MeasureSpec 是由父视图的 MeasureSpec 和 自身的 LayoutParams 一起决定的,接着 View 根据自身的 MeasureSpec 来确定自身测量后的宽/高。
从入口 ViewRootImpl.java 的 performTraversals 方法开始看,它调用 performMeasure 之前做了如下操作:
// ViewRootImpl.java ... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... 复制代码
mWidth, mHeight 表示屏幕的宽高,lp.width, lp.height 表示 DecorView 的宽高属性,对于 DecorView 来说其 width 和 height 都是 match_parent,因此它的尺寸就是屏幕的尺寸,看下 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;
}
复制代码
若布局参数中的宽/高是 MATCH_PARENT, 那么它最终得到的「测量规格」的 mode 是 EXACTLY, size 是屏幕宽/高,MeasureSpec.makeMeasureSpec 方法就是合并了 mode 和 size, 制作了一个 measureSpec 变量;若布局参数中的宽或高是 WRAP_CONTENT, 那么它最终得到的「测量规格」的 mode 是 AT_MOST, size 是屏幕宽/高,乍一看其实尺寸和 MATCH_PARENT 是一样的,所以一般系统定义的控件或者我们自定义 View 都会对 WRAP_CONTENT 进行处理,否则其实它的效果在大部分情况下和 MATCH_PARENT 并无一致;若是其他值(一般用户提供了精确的大小),那么它最终得到的「测量规格」的 mode 是 EXACTLY, size 是用户给定的值。
在求出 DecorView 的「测量规格」后,调用 performMeasure 方法,内部主要是调用了 DecorView 的 measure 方法。由于 measure 方法用 final 修饰了,因此子类无法重写此方法,所有的视图都统一经过 View 中的 measure 这个方法。
// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 前半部分代码主要做了优化,若宽高都不变的情况下
// 或没有强制重新布局的标志位,那就不重新 measure 了
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
复制代码
可以把 measure 方法看做是一个统一的测量入口,做了一些通用的事情,真正的测量是在 onMeasure 方法,这个方法是 View 提供给各个子类去实现的,这里大家能自定义很多测量逻辑,如 LinearLayout 布局容器就是通过此方法获取垂直、水平线性布局时自身的宽/高,反正总之就是一句话, measure 流程就是为了求出自身测量后的宽/高,并保存下来 。现在看下 View 默认的 onMeasure 实现:
// View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码
getSuggestedMinimumWidth 方法就是看下是否有背景,如果有就获取背景的宽度,否则看下是否设置了 minWidth 属性,getSuggestedMinimumHeight同理。在这里直接就无视这两个情况吧,正常来说这个方法返回值是 0, 看下 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;
}
复制代码
根据「测量规格」获取测量模式和测量尺寸, 跳过 UNSPECIFIED 模式,当模式为 AT_MOST 和 EXACTLY 时,最原始的 View 视图无论是指定 match_parent 还是 wrap_content 模式,最后的 size 都是「测量规格」的 size, 所以对于不重写 onMeasure 方法的 View 来说,这两个模式没差别。setMeasuredDimension 也是一个 final 修饰的方法,任何视图都统一将宽/高保存成全局变量以便之后使用。以上就是 View 默认的测量流程,下面看下 ViewGroup 自定义实现的 onMeasure 方法。
由于 DecorView 继承自 FrameLayout,因此接下来的流程其实会调用到 FrameLayout 中的 onMeasure, 不过本文不分析 FrameLayout ,而是分析比较常用的 LinearLayout 重写的 onMeasure 方法,我们只分析垂直方向的:
// LinearLayout.java
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
......
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
......
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
......
}
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
}
复制代码
这里不分析 weight 属性,加上这个属性就有点复杂了。首先遍历子视图,让每个子视图都执行自身的 onMeasure 方法,这个过程在 measureChildBeforeLayout 方法内,一会儿在分析。测量子 View 之后,child.getMeasuredHeight() 就能获得这一波测量后的高度了,mTotalLength 可以看做是目前 child 在竖直方向累加的高度(包括padding, margin)。最后调用 setMeasuredDimension 表示这次测量结束,会记录测量后的宽和高。measureChildBeforeLayout 内部会直接调用 measureChildWithMargins, 此方法是父容器测量子视图的统一入口:
// ViewGroup.java
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int = 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);
}
复制代码
是否还记得之前说的 View 的「测量规格」是由父视图的「测量规格」和自身的布局参数决定的,这里 childWidthMeasureSpec 就是通过 父视图的「测量规格」+ 自身的布局参数 + padding + margin + 已使用的宽/高 决定的。
// ViewGroup.java
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
// 父容器是精确模式 EXACTLY
case MeasureSpec.EXACTLY:
// 子视图有一个精确的尺寸,那么它的测量尺寸也就是这个大小,
// 并且指定它的模式为 EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
// 子视图布局的宽/高是 MATCH_PARENT,那么它的大小就是父容器的大小,
// 并且指定它的模式为 EXACTLY,这里就能看出,一般精确值和 MATCH_PARENT 对应 EXACTLY
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
// 子视图布局的宽度是 WRAP_CONTENT,那么它的大小就是父容器的大小,
// 并且指定它的模式为 AT_MOST,所以一般来说自定义View要重写onMeasure。
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
// 父容器是最大模式 AT_MOST
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;
......
// 最后制作一个子View自身的「测量规格」
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码
上面的注释写的比较清晰了,总结下获取子视图 MeasureSpec 的过程: 如果子 View 布局参数的尺寸是精确值,那么父容器的 mode 不会影响到子视图,子视图都是 EXACTLY 模式 + 精确值尺寸;如果子 View 的宽/高是 MATCH_PARENT, 那么子视图跟随父容器模式 + 父容器尺寸;如果子 View 的宽/高是 WRAP_CONTENT,那么子视图是 AT_MOST 模式 + 父容器尺寸。
在获得子视图的「测量规格」后直接调用子视图的 measure 方法让子视图根据自身的 MeasureSpec 得到测量后的宽高,这个流程和之前讲解的又是一样的。
到此为止 LinearLayout 的 onMeasure 垂直方向大致的流程已经分析完毕。总结下流程: 它会先遍历所有子视图,通过 LinearLayout 的 MeasureSpec 和子视图的 LayoutParams 得出子视图的 MeasureSpec,接着让子视图执行 measure 方法 ,计算子视图测量后的宽/高。通过累加子视图的高度,如果 LinearLayout 是 EXACTLY 模式那么高度还是自身的尺寸,如果 LinearLayout 是 AT_MOST 模式那么对比子视图高度总和取较小一方作为 LinearLayout 的高度。同理,宽度也有这么一个比较过程。关于 weight 属性,最关键的其实是它会让子视图 measure 两次,稍微有点耗时 。
举个栗子,现在有一个布局,LinearLayout 中嵌套一个 TextView 和 View 视图,以下是图解:
2. layout
layout 和 measure 的流程是类似的,直接上源码:
// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// 以下主要是对 requestLayout 处理,暂不深究。
......
}
复制代码
host 就是 DecorView, 直接可以看到 View.layout 方法,虽说此方法没被 final 修饰,但可以看做统一入口,其他子类貌似并没有重写此方法:
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);
......
onLayout(changed, l, t, r, b);
......
}
复制代码
先解释下前半部分的代码,这里的 l, t, r, b 分别表示 自身左边缘与父容器左边缘的距离、自身上边缘与父容器上边缘的距离、自身右边缘与父容器左边缘的距离、自身下边缘与父容器上边缘的距离,根据这些值就能得出自身的宽度为 r - l, 高度为 b - t, 以及自身的四个顶点。 这里比较重要的是 setFrame 方法,里面用全局变量 mLeft, mTop, mRight, mBottom 分别记录了 l, t, r, b, 这个时候它的宽/高算是真正的定下来了(注意 measure 阶段的测量宽高不一定是最终宽高),并且 setFrame 内部调用了, onSizeChanged 方法,于是恍然大悟,怪不得写自定义 View 的时候要在 onSizeChanged 内拿最终宽高。
接下来解释下 layout 方法中的 onLayout 方法。View 类并没有实现 onLayout,也就是说它完全去让子类去实现了,并且 ViewGroup 将此方法设为抽象方法强制去实现,因此只要是父容器都得实现 onLayout 来控制子视图的位置,而子视图没有特殊需求基本不需要去实现此方法。下面看下 LinearLayout 重写的 onLayout 方法,同样只看垂直方向:
void layoutVertical(int left, int top, int right, int bottom) {
......
for (int i = 0; i < count; i++) {
......
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
}
}
复制代码
依然还是省略了一堆代码,只需要解释关键的几个变量。 childLeft 表示子视图的左边缘与父容器的左边缘的距离,这个变量会被padding, margin, gravity 所影响。childTop 表示子视图的上边缘与父容器的上边缘的距离,受到 padding, 已累加的高度影响(因为是垂直布局)。childWidth 和 childHeight 分别是子视图的测量后的宽/高。在 setChildFrame 方法中直接调用了 child.layout, 那么 layout 事件继续往子容器传递,过程和之前解释的一样。
对 layout 做个总结: layout 方法的四个参数决定了自身在父容器内的位置保存为 mLeft, mTop, mRight, mBottom,此方法真正确定了自身的最终宽高。然后如果是继承 ViewGroup 的父容器,那么会重写 onLayout 方法对子视图进行布局确定它们的位置,最后会调用到子视图的 layout 方法,按这种步骤一直传递。
依然举个栗子,,LinearLayout 中嵌套一个 TextView 和 View 视图,以下是图解:
3. draw
performDraw 方法会调到 View 的 draw 方法,重点在于 onDraw 自身的绘制,这也是自定义 View 实现的最关键方法,其次是 dispatchDraw, 此方法在 ViewGroup 被重写主要用来遍历子视图并调用它们的 draw 方法传递绘制事件:
public void draw(Canvas canvas) {
// 绘制背景
drawBackground(canvas);
// 绘制自身内容
onDraw(canvas);
// 遍历子视图让它们绘制 draw
dispatchDraw(canvas);
// 画装饰(前景,滚动条)
onDrawForeground(canvas);
// 绘制默认焦点高亮
drawDefaultFocusHighlight(canvas);
}
复制代码
draw 调用流程是比较清晰简单的,但它真正的实现是很复杂的,这一块是自定义 View 的关键部分,需要学很多东西呀。。不过从这里能看出自定义 View 主要是重写 onDraw 以及 onMeasure 方法,而自定义 ViewGroup 主要是重写 onMeasure 以及 onLayout 方法。
三、总结
用文字的形式表达下整个绘制流程:
整个绘制流程的入口是 ViewRootImpl.performTravesals 方法,绘制的先后顺序是 measure, layout, draw.
performMeasure 通过计算得出 DecorView 的 MeasureSpec 然后调用其 measure 方法,此方法是 View 类的统一入口,主要是做了判断是否要测量和布局,如果需要则直接调用重写的 onMeasure 方法(因继承 ViewGroup 容器的布局特性所决定的)根据 MeasureSpec 对自身进行测量得出宽/高。父容器会遍历所有子视图,根据自身的 MeasureSpec 和 子视图的 LayoutParams 决定子视图的 MeasureSpec, 并调用子视图的 measure 方法传递测量事件,直到传递到整个 View 树的叶子为止。
performLayout 从 View 树的顶端开始,依次向下调用 layout 方法来确认自身在父容器内的位置,这时最终的宽高被确认,然后调用重写过的 onLayout 方法(根据布局特性重写)来确认所有子视图的位置。
performDraw 也是按照前面测量和布局的思路传递在整个 View 树中,onDraw 绘制自身的内容是实现自定义View的最关键方法。
View 相关的常见问题:
- requestLayout 为什么耗时?View 调用 requestLayout 方法后,会自下而上传递事件,将设置每层 View 的测量和布局的标志位,最后会调用 performTravesals 方法基本会重新走一遍整棵 View 树的绘制流程 measure, layout, draw。
- invalidate 和 postInvalidate?这两个重绘方法也会调用到 performTravesals, 但不会设置测量和布局的标志位,所以只会执行 draw 过程。invalidate 在主线程中执行,postInvalidate 是异步绘制,通过 handler 回调到主线程。
- onMeasure 多次调用的情况?绘制过程中可能会出现多次 measure 的情况,如父容器 LinearLayout 使用了 weight 属性。
- onSizeChanged 调用时机?此方法在 layout 中调用,这时已经确认了最终的宽/高,因此这个方法取宽高的时机比 onMeasure 取宽高的时机靠谱。
- RelativeLayout 和 LinearLayout 性能对比?一般层级比较多的情况下推荐使用 RelativeLayout,因为它可以有效减少 LinearLayout 的层级问题,但只有一层的情况下推荐用 LinearLayout,因为 RelativeLayout 总是会 measure 两次,而 LinearLayout 不设置 weight 的话只会 measure 一次。RelativeLayout 中优先用 padding 而不是 margin,对margin 的处理比较耗时。
- 还有啥问题呢。。
最后推荐 ConstraintLayout,还没有真正去研究这个约束布局,但它基本一层就能搞定一个布局,还管你什么层级的性能问题吗?应该是完爆其他布局的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- View绘制流程源码分析
- R语言绘制流程图(二)
- Android进阶(五)View绘制流程
- View的绘制-measure流程详解
- View的绘制-layout流程详解
- View的绘制-draw流程详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Programming in Haskell
Graham Hutton / Cambridge University Press / 2007-1-18 / GBP 34.99
Haskell is one of the leading languages for teaching functional programming, enabling students to write simpler and cleaner code, and to learn how to structure and reason about programs. This introduc......一起来看看 《Programming in Haskell》 这本书的介绍吧!