1、 ViewRoot 和 DecorView 介绍
ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowMnager 类 和 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 工作流程.png
由上图可知,performTraversals 会依次调用 performMeasure、performLayout 和 performDraw方法。这三个方法会完成顶级 View 的 measure、layout 和 draw 这三个流程。
其中,performMeasure 中会调用 measure 方法,在measure 方法中又会调用 onMeasure,然后在 onMeasure 才会对所用子元素进行 measure 过程,这时候,就从父容器传递到了子元素,也就是完成了一次 measure 过程,接着子元素会重复父容器的 measure 过程,如此反复直到完成整个 View 树的遍历。performLayout 和 performDraw 类似,唯一不同的是,performDraw 的传递 过程是在 draw 方法中,通过 dispatchDraw 来实现的。
measure 过程决定了 View 的宽高,Measure 完成之后,可以通过 getMeasureWdith 和 getMeasureHeight 方法获取到 View 测量后的宽高,一般情况下,这就是 View 最终的宽高。Layout 过程决定了 View 四个顶点的坐标和最终 View 的宽高,完成之后,可通过 getTop、getBottom、getLeft、getRight 来得到 View 四个顶点的位置,并且可以通过 getWidth 和 getHeight 来得到 View 最终的宽高。最后,Draw 方法决定了 View 的显示,只有 draw 方法完成后 View 的内容才能呈现在屏幕上。
DecorView 的结构:
decorView.png
如图所示,DecorView 做为顶级 View,一般情况下它的内部都会包含一个竖直方向的 LinearLayout,在这个 LinearLayout 里面有上下两个部分(具体情况和 Android 版本及主题有关)。我们设置的布局就是下面的内容栏,其 id 为 content。我们可以通过:
ViewGroup content = findViewById(R.android.id.content);
得到这个 content,然后我们可以通过 content.getChildAt(0) 得到我们设置的 View。我们还需要知道,DecorView 是一个 Fragment,View 的事件多要经过DecorView 才能传递给我们的 View。
2、理解 MeasureSpec
为了更好的理解 View 的测量过程,我们还要理解 MeasureSpec。MeasureSpec 决定了 View 的规格尺寸,但是 MeasureSpec 还受到父容器的影响。在测量的过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后在根据这个 measureSpec 来测量出 View 的宽高。
2.1 MeasureSpec
MeasureSpec 代表了一个 32 位的 int 值,高 2 位 代表 SpecMode,低 30 位代表 SpecSize。
- SpecMode 是测量模式
- SpecSize 是测量尺寸
MeasureSpec:
/**
* MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求
* MeasureSpec由size和mode组成。
* 三种Mode:
* 1.UNSPECIFIED
* 父不没有对子施加任何约束,子可以是任意大小(也就是未指定)
* (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,模式为UNSPECIFIED
* 2.EXACTLY
* 父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。
* (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)
* 3.AT_MOST
* 子最大可以达到的指定大小
* (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)
*
* MeasureSpecs使用了二进制去减少对象的分配。
*/
public class MeasureSpec {
// 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位)
private static final int MODE_SHIFT = 30;
// 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
// (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 0向左进位30,就是00 00000000000(00后跟30个0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 1向左进位30,就是01 00000000000(01后跟30个0)
public static final int EXACTLY = 1 << MODE_SHIFT;
// 2向左进位30,就是10 00000000000(10后跟30个0)
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* 根据提供的size和mode得到一个详细的测量结果
*/
// measureSpec = size + mode; (注意:二进制的加法,不是十进制的加法!)
// 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
// 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
/**
* 通过详细测量结果获得mode
*/
// mode = measureSpec & MODE_MASK;
// MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
// 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* 通过详细测量结果获得size
*/
// size = measureSpec & ~MODE_MASK;
// 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
/**
* 重写的toString方法,打印mode和size的信息,这里省略
*/
public static String toString(int measureSpec) {
return null;
}
}
代码中有详细注释,主要说一下 SpecMode:
- UNSPECFIED
父容器不对 View 有任何限制,要多大给多大,一般用于系统内部。 - EXACTLY
父容器已经检测出 View 所需要的大小,这时候 View 的最终大小就是 SpecSize 所指定的值。这个模式对应 LayoutParams 中的 match_parent和具体数值两种模式。 - AT_MOST
父容器制定了一个可用大小即 SpecSize,这个时候, View 的最终大小不能大于这个值,具体是多少要看 View 的具体实现,对于LayoutParams 中的 wrap_content。
2.2 MeasureSpec 和 LayoutParams 的对应关系
系统内部是通过 MeasureSpece 来进行 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 来共同决定,MeasureSepc 一旦确定后,onMeasure 就可以确定 View 的测量的宽高。
对于 DecorView 来说,在 ViewRootImp 中的 measureHierachy 方法中有如下代码,它展示了 DecorView 的 MeasureSpec 的创建过程,其中 desiredWindowWidth 和 desiredWindowHeight 和屏幕尺寸:
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
getRootMeasureSpec 方法如下:
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
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 的 MeasureSpec 的产生过程遵守如下规则,根据它的 LayoutParams 中的宽/高参数来划分:
- LayoutParams.MATCH_PARENT
精确模式,大小就是窗口的大小。 - LayoutParams.WRAP_CONTENT:
最大模式,大小不定,但是不能超过窗口大小。 - 固定大小(如 100 dp):精确模式,大小为 LayoutParams 中指定的大小。
对于我们布局中的 View, View 的 measure 过程由 ViewGroup 传递而来,ViewGroup 的 measureChildWidthMargins 方法如下:
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 有关,此外还和 View 的 margin 及 padding 有关,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,具体代码如下:
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
根据代码,普通 View 的创建规则如下:
View 的 measureSpec 创建规则.png
主要规则如下:
- 当View采用固定宽/高时,不管父容器的Measure是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams中的大小。
- 当View的宽/高是match_parent时,如果父容器是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
- 当View的宽/高是wrap_content时,不管父容器是最大模式还是精确模式,View的模式总是最大模式,并且其大小不会超过父容器的剩余空间。
- UNSPECIFIED模式主要用于系统内部多次Measure的情形,一般来说,不需要关注此模式。
根据上表,我们只要指定 父容器的 MeasureSpec 和子元素的 LayoutParams ,就可以快速的确定子元素的 MeasureSpec,有了 MeasureSpec 就可以进一步确定子元素测量后的大小。
3、View 的工作流程
View 的工作流程主要是 measure、layout、draw 三大流程,也就是测量,布局和绘制。
measure: 确定 View 的测量宽高。
layout: 确定 View 的最终宽高和四个顶点的位置。
draw: 将 View 绘制到屏幕上。
3.1 View 的measure 过程
View 的 measure 过程有 measure 方法来完成,measure 方法是一个 final 类型的方法,这意味着此类方法不能被子类重写,在 View 的 measure 方法中会去调用 View 的 onMeasure 方法,因此只需要看 onMeasure 的实现,View 的 onMeasure 方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension 方法会设置 View 的宽高 的测量值,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;
}
由上可知,getDefaultSize 这个方法逻辑很简单,对我们来说,只需要 AT_MOST 和 EXACTLY 这两种情况。简单的理解,getDefaultSize 返回的大小就是 measureSpec 和 specSize,而这个 specSize 就是 View 测量后的大小,测量后的大小是在 layout 阶段决定的,这里要加以区分,但是几乎全部情况下 View 的测量大小和最终大小是相等的。
至于 UNSPECCIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下, View 的大小为 getDefaultSize 的第一个参数 size,即宽高分别为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 这两个方法的返回值,源码如下:
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
这里只分析,getSuggestedMinimum 方法的实现,从其代码可以看出,如果 View 没有设置背景,那么 View 的宽度为 mMinWidth,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值,如果这个属性不指定,那么 mMinWidth 则默认为 0,如果 View 指定了背景,则 View 的宽度为 max(mMinWidth,mBackgroudn.getMinimumWIdth())。
Drawable 的 getMinimumWidth 方法
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
从上面可以看出,getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就返回 0。
getSuggestedMinimumWidth 的逻辑总结如下:如果 View 没有设置背景,那么返回 android:minWidth 这个属性所指定的值,这个值可以是 0,如何 View 设置了背景,则返回 android:minWidth 和背景的最小宽度这两者中的最大值。getSuttestedMinimumWidth 个 getSuggestedMinimumHeight 的返回值就是 View 在 UNSPECIFIED 情况下的测量宽高。
从 getDefaulSize 方法的实现来看,View 的宽高由 specSize 决定,所以我们可以得出结论:直接继承 View 的自定义控件需要重写onMeasure 方法并设置 wrap_content 的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。如果 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽高等于 specSize,这种情况下, View 的 specSize 是 parentSize,而 parentSize 是父容器目前可以使用的大小,也就是父容器当前剩余的空间的大小,很显然, View 的宽高就等于父容器当前空间的大小,这种效果和在布局中使用 match_parent 完全一致,为了解决这个问题,在onMeasure 中会给 View 一个默认的宽高值:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int defaultWidth = 100;
int defaultHeight = 100;
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, defaultHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, defaultHeight);
} else {
widthSpecSize = Math.min(widthSpecSize, heightSpecSize);
heightSpecSize = Math.min(widthSpecSize, heightSpecSize);
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
上面代码中,给 View 指定了一个默认的内部宽高值(mWidth 和 mHeight),并在 wrap_content 时设置这个宽高,对用非 wrap_content 情形,沿用系统的测量值即可。
ViewGroup 的 measure 过程
对于 ViewGroup 来说,除了完成自己的 measure 过程之外,还会遍历去调用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是, ViewGroup 是一个抽象类,因此没有重写 View 的 onMeasure 方法,但是它提供了一个 measureChildren 的方法,如下:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
由上面的代码可知, ViewGroup 会对每个子元素进行 measure,measureChild 方法如下:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上面代码的主要思想就是,取出子元素的 LayoutParams,然后通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法进行测量。
我们知道,ViewGroup 没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如 LinearLayout 、Relativelayout 等,这是因为不同的 ViewGroup 子类有不同的布局特性,因此其细节各不相同。
LinearLayout 的 onMeasrue 方法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
直接看竖直布局的测量过程:
// 代码省略 ...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LayoutParams lp = (LayoutParams)child.getLayoutParams();
final float childWeight = lp.weight;
measureChildBeforeLayout(child,i,widhtMeasureSpec,0,heightMeasureSpec,totalWeight == 0 ? mTotalLenght : 0);
if( oldHeight != Integer.MIN_VALUE){\
lp.height = oldHeight;
}
final int childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(
Math.max(0, childHeight), MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
其测量过程的代码较多,主要思想就是,系统会遍历子元素并对子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,这样各个子元素就开始依次进入 measure 过程,并且系统会通过 mTotalLength 这个变量来存储 LinearLayout 在竖直方向的初步高度,没测量一个子元素,mTotalLength 就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方法的 margin 等。当子元素测量完成后,LinearLayout 会测量自己的大小,源码大致如下:
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
...
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
View 的 measure 过程是三大流程中最复杂的一个, measure 完成之后,通过 getMeasuredWidht/height 方法就可以得到 View 的测量宽高。
现在考虑一个情况,当我们要在 onCreate 或者 onResume 中获取 View 的宽高时,这样是无法正确得到 View 的宽高信息的,因为 View 的 measure 过程和 Activity 的周期不是同步的,目前有四种方法解决这个问题:
- Activity/View #onWindowFocusChanged
onWindowFocusChanged 这个方法的含义是: View 已经初始化完毕了,宽高已经准备好了,这时候去获取宽高是没有问题的。需要注意的时,这个方法会被多次调用,具体来说就是,Activity 继续执行和暂停执行都会调用这个方法。
其使用方法如下:
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int height = view.getMeasureHeight();
int widht = view.getMeasureWidth();
} else {
Log.e(Tag, "onWindowFocusChanged:" + "false");
}
}
- view.post(runnable)
通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候,View 已经初始化好了,典型代码如下:
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
- ViewTreeObserver
通过使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变的时候,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的机会。值得注意的是,伴随着View树的状态改变等,onGlobalLayout会被多次调用。代码如下:
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = tv.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = tv.getMeasuredWidth();
int height = tv.getMeasuredHeight();
}
});
}
- view.measure(int widthMeasureSpec,int heightMeasureSpce)
通过手动对 View 进行 measure 来得到 View 的宽高,这种方法比较复杂,要分情况处理,根据 View 的 LayoutParams 来分:
(1) match_parent:
直接放弃,因为无法知道 parentSize,而 measure 过程需要知道这个值。
(2 )具体的数值(dp/dx):
比如宽高都是 100px,如下 measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpce(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpce(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
(3) wrap_content
如下 measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpce((1 << 30) -1,MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpce((1 << 30) -1,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
因为 View 的尺寸使用 30 为二进制表示,也就是说最大是 30 个1(即 2^30 - 1),也就是 (1 << 30) -1,在最大化模式下,我们用 View 理论上能支持的最大值去构造 MeasureSpec 是合理。
3.2 View 的 layout 过程
Layout 的作用 用来确定子元素的位置,当 ViewGroup 的位置确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。layout 方法主要是确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置。
layout 方法如下:
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<OnLayoutChangeListener> listenersCopy =
(ArrayList<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;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom
这四个值,View 的四个顶点确定之后,那么 View 在父容器中的位置也就确定了,接着调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法,接下来我们看一下 LinearyLayout 中 onLayout 是如何实现的:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
也就是 onLayout 的实现还是和布局有关,这里以垂直为例:
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
这个方法的主要逻辑如下:首先遍历所有子元素并调用 setChildFrame 方法来为子元素指定对应的位置,其中 childTop 会逐渐增大,这就意味着后面的子元素会被放置到靠下的位置,这刚好符合垂直 LinearLayout 的特性,而下面的 setChildFrame 只是调用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样依次调用就确定了整个 View 树的 layout 过程。
setChildFrame 如下:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
setChildFrame 中的 width 和 height 实际上就是子元素的测量宽高,上面的代码如下:
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
....
setChildFrame(child, childLeft + getLocationOffset(child), childTop,
childWidth, childHeight);
而在 layout 中,会通过 setFrame 去设置子元素的四个顶点的位置,在 setFrame中有如下赋值语句:
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
这样一来 子元素的位置就确定了。
现在说一下 View 的测量宽高和最终宽高有什么区别,也就是 View 的 getMeasureWidth 和 getWidth 有什么区别。先看一下 getWidth 和 getHeight 的方法的实现:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
也就是 getWidth 和 getHeight 返回的是测量宽高,虽然在 View 的默认实现中,测量宽高和最终宽高是相等的,但是需要知道的是,测量宽高形成与 measure 过程,而最终宽高形成与 layout 过程,也就是赋值时机不一样,我们在日常开发中,可以认为这两个是相等的。
下面举例说明为什么是不相等的:
public void layout(int l,int t,int r,int b){
super.layout(l,t,r+100,b+100);
}
重写 layout 方法,导致最终宽高比测量的大 100px。
3.3 View 的 draw 过程
draw 过程比较简单,主要作用就是将 View 绘制到屏幕上面,View的 draw 过程如下:
- 绘制背景 background.draw(canvas)。
- 绘制自己 (onDraw)。
- 绘制 children (dispatchDraw)。
- 绘制装饰 (onDrawScrollBars)。
draw 源码如下:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
View 的绘制过程是通过 dispatchDraw 来实现的, dispatchDraw 会遍历调用所有子元素的 draw 方法,如此 draw 会一层一层的传递下去,View 有一个特殊的方法,setWillNotDraw,源码如下:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
也就是,一个 View 如果不需要绘制任何内容,那么设置这个标记位为 true 后,系统会相应的进行优化,View 默认没有启用这个标记位,ViewGroup 默认启用这个标记位。这个标记位的意义是:当我们自定义控件继承 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记以便于系统优化。相反,我们的 ViewGroup 需要绘制时,要手动关闭这个标记位。
具体参考 《Android 开发艺术探索》View 工作原理。
网友评论