View的测量
前言
我们做Android开发的目的就是把产品设计的东西运行到我们的手机上,说白了就是把设计图搬到我们的手机上,其中跟我们打交道最多的就是View, 所以我们有必要了解它是个什么东西,又是怎么把我们需要的东西展现在手机屏幕上,这就涉及到了View的测量、布局和绘制。这里我将分三个章节来一一向你展示,本节我们先来分析View的测量。在分析源码之前,我们先问自己几个问题,什么是测量?测量是干什么的?那些东西需要测量?从哪里开始测量?带着这些问题我们开始进行今天的答疑之旅
1. 测量是什么?它是干什么的?
测量就是描述一个物体所占据的空间,也就是它的长宽高,是一个物体本身的属性
2. 为什么需要测量
回答这个问题之前我们要明白我们要干什么,我们是要把我们的xml文件里面的view绘制到手机屏幕上,既然我们要把view绘制到屏幕上那我们总得知道我们绘制的view有多大吧,这就是我们为什么需要测量
3. 那些东西需要测量?
我们绘制到屏幕上的view要么是单个View,要么是一组View即ViewGroup,因此需要测量的也就是View和ViewGroup
4. 从哪里开始测量?
解答了上面的问题接下来我将为大家讲解从哪里开始测量以及单个View和ViewGroup(一组View)怎么测量
从哪里开始测量
刚我也说了我们测量是要把xml文件中的view,添加绘制到手机屏幕上,所以测量的开始从添加view开始,通过Activity的启动流程到View的显示我们知道,view的添加是在ViewRootImpl中进行的,调用的是addView() -> setView() -> requestLayout(),不明白的可以看我之前的文章,接下来我们从requestLayout()开始分析
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true; //大家在这里先记住这个标记,在后面很重要
scheduleTraversals();
}
}
void scheduleTraversals() {
.... 省略部分代码
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
performTraversals();
}
通过查看源码发现其内部调用的是performTraversals(), 这又是何方神圣呢?我们进入代码在仔细看个清楚
private void performTraversals() {
//注意 mLayoutRequested直接决定了layoutRequested,也在很大程度上决定了performMeasure()
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
boolean windowShouldResize = layoutRequested && windowSizeMayChange
&& ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() != mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() != mHeight));
// 满足任意添加就会进行测量
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
//获取顶层布局的宽高测量规格,执行测量方法
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
....
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
//测量走了,layout方法一般也会走
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
....
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
//在这里进行页面的绘制
performDraw();
}
}
原来我们的测量、布局和绘制都是在这个方法里面完成的,今天我们分析的是测量,布局和绘制等到下个章节在讲,接下来我们就看看performMeasure()
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//注意这个mView其实就是decorView, 本质上是一个ViewGroup
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
接下来会走到ViewGroup的measure(), 通过源码发现ViewGroup没有measure(), 所以我们只有查看它的父类View, 我们来看看View的measure()做了什么
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
View的measure方法判断了一下View的模式,就直接调用onMeasure(), 是不是觉得很熟悉,没错我们自定义view的时候就需要复写该方法,之前我们也说过测量有View的测量和ViewGroup的测量,我们先来看看View的测量即onMeasure()
View的测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
View的onMeasure()中得到一个默认值就直接设置给了view,在获取默认值时调用了getSuggestedMinimumWidth(),我们来看看这个方法里面做了什么
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
根据方法名我们知道得到的是一个建议的最小值,首先我们要明白mBackground是谁,mBackground是我们给view设置的背景,为什么要在这里判断mBackground是否为null呢?这是因为如果我们给view设置了背景,为了凸显出该view设置了背景,系统就会给背景设置一个最小宽度。 mMinWidth这是从哪里来的呢?别急这个值是你自己设置的,我们写的xml文件在给手机屏幕做适配的时候会用到,如:android:minWidth="20dp",如果不设置的话默认为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;
}
注意这里AT_MOST和EXACTLY模式下得到的值是一样的,接下来我们再来看看setMeasuredDimension()设置测量的view的宽高
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
//判断父view是否有视觉模式,如果有在加上视觉模式所需要的宽高
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { //最终在这里对view的宽高进行保留
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
在这里会保存测量的宽高,后面我们使用的getMeasureWidth()、getMeasureHight()得到的值就是在这里保留的值。至此view的测量到此结束,我们来看看ViewGroup的测量吧,ViewGroup是一个抽象类,onMeasure()也是抽象的,我们看看他的子类,我们知道Android有四大布局,即FrameLayout、RelativeLayout、LinearLayout及AbsoultLayout, AbsoultLayout绝对布局,这个我们很少用到,FrameLayout是最简单的布局,我们就先看看它内部是怎么实现的。
ViewGroup的测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//1、获取所有的子类
int count = getChildCount();
//2、判断子类里面的宽高是否有设置match_parent
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
//将存放width或height为match_parent的view集合清空
mMatchParentChildren.clear();
//3、定义变量存储获取的子类的最大宽高
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
//4、遍历所有的子类,获得其宽高,取出子类中最大的宽和高
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//5、如果子类的宽高有设置match_parent,则将其添加到数组中
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
//6、将子类获得的最大值加上父类设置的padding值,得到一个最大值
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// 7、再次检验,将获得的最大值和建议的最小值进行比较取得最终的最值
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
// 8、设置frameLayout的宽高,frameLayout的宽高是通过测量子类的宽高,得其最大的宽高在加上padding,在进过一系列的判断才最终得到的
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
// 9、获取子类中宽或高设置match_parent的个数,如果其个数大于0,我们就需要对这些view重新测量,
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
看完FrameLayout的测量不知道你们有没有理解清楚,心里是否还有疑问?有人说你说的已经很清楚了没有什么问题的,哪好我在问你们一个问题,子View为什么需要进行二次测量,是这样的当view的宽或者高设置match_parent的时候,子类自己也不知道自己的宽高到底是多少,只知道和父类一样就行了,当父类的宽高确定了之后,就需要对那些设置了match_parent的view重新测量,设置宽高。
总结
源码分析完了接下来我们来回顾总结一下,测量是从ViewRootImpl中的requestLayout()方法发起,经过一系列的操作最终调到performTraversals(), 在该方法中会依次调用performMeasure()、performLayout()、performDraw()。 在requestLayout()中有一个参数mLayoutRequested很重要,它很大程度上决定了performMeasure()和performLayout()是否调用,对performDraw()没有什么影响,我们先来看performMeasure(), 发现其内部最终调用的是View的measure(),最终调用到我们最熟悉的onMeasure(). 这时我们就分别介绍了View的onMeasure()和ViewGroup的onMeasure()
1. View的onMeasure()
* View的onMeasure()很简单,直接调用setMeasuredDimension(), 设置View的宽高,通过setMeasuredDimensionRaw()保留我们设置的宽高,使用的时候通过getMeasureWidth()、getMeasureHeight()来获取我们设置的宽高
2. ViewGroup的onMeasure()
* ViewGroup的onMeasure()很复杂,ViewGroup的宽高是通过子类的宽高来决定的,所以需要先测量所有子类的宽高,得到子类宽高的最值,然后通过子类的宽高在加上一些padding值及其他条件的校验最终得出ViewGroup的宽高,在设置ViewGroup的宽高,然后再重新测量子类宽高设置为match_parent的view
接下来我们看一张就一目了然了

requestLayout()、invalidate()、 postInvalidate()的区别
其实这个跟本章内容不是很搭,为什么在这里提出来呢,因为他们之间的区别在本文的代码里面就能够很好的区别开来,源码不是很多我就不一一展示了,想弄清楚的同学可以自己翻源码好好看看。
* requestLayout(): 我们就不用多说了,本文的起点就是从它开始的,通过它才有了后面View的measure、layout和draw。
* invalidate(): 这个方法我们也不陌生,用来更新界面,这时我们就要问了它更新界面的时候还会走measure和layout吗?我想这个你自己也不清楚吧,进入源码我们可以看到View的invalidate()会递归的调用 parent.invalidateChildInParent(),最终调用ViewRootImpl的invalidateChildInParent(), 通过查看源码得知ViewRootImpl的invalidateChildInParent()内部调用的是invalidate(), 注意这个invalidate()是ViewRootImpl中的,而invalidate()内部调用的是scheduleTraversals(),看到这里我们就熟悉了下面的操作了,看到这里的同学就知道了接下来就要执行performTraversals(),在接着执行performMeasure()、performLayout()和performDraw()了,慢着,这里需要暂停一下,还记得我在前面一直说的要你注意mLayoutRequested这个标记吗?当我们requestLayout()之后会将其置为false,只有在调用requestLayout的时候将其置为true。当你调用invalidate()的时候该标记依然为false,就不会走performMeasure()和performLayout(),直接走的是performDraw()。
* postInvalidate(): 其内部还是调用的是invalidate(), 只不过它可以在子线程通知更新UIblo
实战应用
至此我们对View的测量流程已经有了大致的了解了,可能有人就要问了,哪这些对我们实际开发中有什么用呢?实际开发的时候有用到吗?同学别急,这个大有用处,听我为你娓娓道来。在项目中根据需求我们需要自定义view,这里我们给自定义的View分为两种,一种是根据系统已有的view,然后根据需要自定义我们所需要的,另一种是完全的自定义。
1. 根据系统已有View自定义我们需要的View
举个例子:我们需要一个方形的ImageView,无论你宽高怎么设置我得到的都是方形的。看到这个需求我们首先想到的是系统给我们提供了Imageview,但系统的满足不了我们的需求,ImageView的宽高究竟是怎么测量的我们不得而知,但是我们可以获取系统帮我们测量的imageview的宽高,然后我们在将它的宽高设置成一样的就行了
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredHeight = getMeasuredHeight();
int measuredWidth = getMeasuredWidth();
int result = 0;
if (measuredHeight > measuredWidth) {
result = measuredHeight;
} else {
result = measuredWidth;
}
setMeasuredDimension(result, result);
}
2. 系统没有定义的,完全由我们自定义
由于是完全的自定义,系统也没法帮我们测量,需要我们自己计算自己所需要的宽高,所以不需要super.onMeasure(), 在父类所限制的测量模式下计算出我们所需要的宽高,最后调用setMeasuredDimension()设置我们的宽高。另一种方式是直接计算我们需要的宽高,计算完了之后调用resolveSize(size, measureSpec)校验我们计算的宽高,其中measureSpec是父类对子类的宽高测量规格。然后在调用setMeasuredDimension()设置宽高。