首先扯点别的:“光阴似箭,日月如梭”,这句话小学就知道了,随着年龄的增长,越来越觉得如此,人生如白驹过隙。毕业工作快一年了,但是感觉自己Android方面的基础知识还是不扎实,所以看看开发艺术探索,巩固提高自己。
View 的measure过程:measure 过程确定了View的测量宽/高。measure完成以后,就可以通过getMeasuredWidth和getMeasuredHeight来获取View测量后的宽高了,几乎所有的情况下它都等于View的最终宽高。
为了更好的理解View的measure过程,首先要了解MeasureSpec这个概念。MeasureSpec代表一个32位的int值,高两位代表SpecMode,低30位表示SpecSize。SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。MeasureSpec参与了View的measure过程,在measure过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。
MeasureSpec类是View类中的一个静态内部类,下面看一下MeasureSpec中一些常量和方法。
private static final int MODE_SHIFT = 30;
//MODE_MASK1100 0000 0000 0000 0000 0000 0000 0000
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//三种测量模式
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
//生成的MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
//返回生成的MeasureSpec,就是size和mode的按位与的结果。
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
三种测量模式说明
对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。如下图所示:
View的测量过程是在measure方法中完成的,measure方法是一个final类型的方法,子类不能重写此方法。View真正的measure工作是在onMeasure方法中完成的,子类可以重写onMeasure方法来实现测量工作。
final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//...
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 测量View及其内容来确定View的测量宽高。子类应该重写这个方法来提供对其内容真实有效的测量。
* 约定:当你重写这个方法,你必须调用 {setMeasuredDimension(int, int)} 方法来存储
* 该View的测量宽高。否则{measure(int, int)}方法会抛出IllegalStateException。
* 调用父类的{onMeasure(int, int)}方法是一种有效的方式。
*
* onMeasure的基类实现默认为背景大小,除非MeasureSpec允许更大的大小。 子类应覆盖
* {onMeasure(int,int}以提供更好的内容测量。
*
*
* 如果该方法被重写了。子类应该确保View的测量宽高大于等于View的最小宽高
* getSuggestedMinimumWidth(), getSuggestedMinimumHeight()。
* 。
*
* @param widthMeasureSpec 父view施加的水平空间约束,封装在
* {@link android.view.View.MeasureSpec}中。
*
* @param heightMeasureSpec 父view施加的竖直空间约束,封装在
* {@link android.view.View.MeasureSpec}中。
*
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
setMeasuredDimension方法会储存测量的宽高,看一下getDefultSize方法。
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;
//在这两种模式下都返回specSize。
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
}
如果第二个参数measureSpec类型是AT_MOST或者EXACTLY,就返回specSize。这个specSize就是测量后的宽高。如果measureSpec类型是UNSPECIFIED类型,就直接返回第一个参数size,即宽高分别为getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的返回值。
我们看一下getSuggestedMinimumWidth()方法的实现,getSuggestedMinimumHeight()的原理是一样的。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ?
mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果View没有设置背景(Drawable),宽度就是mMinWidth ,对应于android:minWidth这个属性指定的值。如果这个属性不指定,那么mMinWidth默认为0。如果View设置了背景,那么View的宽度就是就是mMinWidth和mBackground.getMinimumWidth()两者中间的最大值。看一下mBackground.getMinimumWidth()的实现。
Drawable的getMinimumWidth方法
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
如果Drawable的原始宽度不为0,就返回Drawable的原始宽度,否则返回0。
总结:从getDefaultSize方法看,View的宽高由specSize决定。直接继承View需要重写onMeasure方法,并且当View的宽高使用wrap_content方式的时候,需要给View设置一个默认的大小,因为在getDefultSize方法中,AT_MOST模式下返回的也是specSize。根据上面的表格可以看出,在AT_MOST模式下,specSize就是parentSize,也就是父容器最大可用空间。这种效果跟使用match_parent效果一样。那如何解决呢?就是在重写onMeasure 方法的时候,如果View使用了wrap_content(测量模式是AT_MOST),那我们就提供一个默认的宽高。举个例子,如下所示:
//默认大小
private static final int DEFAULT_SIZE = 200;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecModel = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecModel = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecModel == MeasureSpec.AT_MOST &&
heightSpecModel == MeasureSpec.AT_MOST) {
setMeasuredDimension(DEFAULT_SIZE, DEFAULT_SIZE);
} else if (widthSpecModel == MeasureSpec.AT_MOST) {
setMeasuredDimension(DEFAULT_SIZE, heightSpecSize);
} else if (heightSpecModel == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, DEFAULT_SIZE);
}
}
上面就是View的测量过程了。
ViewGroup过程除了完成自己的measure过程以外,还要遍历调用所有子元素的measure方法,各个子View再递归执行这个过程。和View不同的是,ViewGruop是一个抽象类,没有重写onMeasure方法,但是它提供了一个叫measureChilden 的方法。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//遍历所有的子View,
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//测量子View的大小。
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChildren方法中会通过调用measureChild对每一个子View进行测量。
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);
}
measureChild方法会根据ViewGroup的测量规格和子View的LayoutParams生成子View的测量规格,然后传递给View的measure方法进行测量。
ViewGroup是一个抽象类,并没有实现onMeasure方法,需要子类(比如LinearLayout,RelativeLayout)去实现onMeasure方法。下面通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
//竖直方向测量
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
//水平方向测量
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
我们选择竖直布局的LinearLayout测量过程measureVertical方法。
我们先看一下measureVertical方法中的部分代码
//LinearLayout 最终高度
mTotalLength = 0;
//LinearLayout 最大宽度
int maxWidth = 0;
//测量模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//遍历所有的child
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//获取child的LayoutParams
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//标记是否使用剩余的空间,当我们使用weight的并且高度指定为0
//的时候useExcessSpace为true。我们这里不关注这种方式
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
//...
} else {
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
//注释1处
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
//注释2处
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
//注释3处,计算最终高度
mTotalLength = Math.max(totalLength, totalLength + childHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
boolean matchWidthLocally = false;
//如果测量模式为MeasureSpec.EXACTLY并且存在child的width是MATCH_PARENT
if (widthMode != MeasureSpec.EXACTLY
&& lp.width == LayoutParams.MATCH_PARENT) {
// 至少有一个child想和LinearLayout一样宽,我们在这里设置一个标记
//指示当我们知道了我们的宽度的时候,我们需要重新测量这个child。
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
//注释4处,获取child的测量宽度
final int measuredWidth = child.getMeasuredWidth() + margin;
//注释5处,为最大宽度赋值
maxWidth = Math.max(maxWidth, measuredWidth);
}
//注释6处
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
//注释7处
maxWidth += mPaddingLeft + mPaddingRight;
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
//注释8处
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
上面代码的注释1处,调用了measureChildBeforeLayout方法来测量child的宽高。
LinearLayout的measureChildBeforeLayout方法
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
//调用LinearLayout的measureChildWithMargins方法
meaureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
LinearLayout的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方法完成child的测量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
到这里我们完成了一个child的测量工作,然后我们回到LinearLayout测量过程measureVertical方法
的注释2处,获取child的测量高度,然后在注释3处,累加mTotalLength,直到遍历完所有的childView。注释4处,获取child的测量宽度,然后在注释5处,为最大宽度maxWidth重新赋值。
在注释6处,mTotalLength加上数值方向上的padding,然后计算最终高度heightSize。在注释7处,计算最终的宽度maxWidth。然后在注释8处调用setMeasuredDimension方法,保存LinearLayout的测量宽高。
LinearLayout的measure过程到此结束。
参考