概述
View的绘制包括measure,layout,draw三个过程,即测量,布局,绘制。measure是对view的宽高进行测量,layout是确定view的四个顶点,即确定view的位置,draw是将最终的view绘制到屏幕上。
measure过程
如果只是一个单一的view,只需要对自身进行measure就足够了,但如果是一个viewgroup,除了对自身进行measure外还需要遍历它所有的子view,并调用其子view的measure,然后子view再执行以上递归操作。所以view的measure要分两个部分进行。
1.单一view的measure过程
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//。。。
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//调用onMeasure进行具体的测量
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
}
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
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
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
//将最终测量的宽高进行保存
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
可以看到,单一view的总体测量过程很简单,就是在onMeasure中测量后把宽高保存在mMeasuredWidth,mMeasuredHeight两个本地变量中。接下来看一下具体的测量过程。
//参数介绍
// size:默认大小 measureSpec:测量规格
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
// 获取测量模式
int specMode = MeasureSpec.getMode(measureSpec);
// 获取测量大小
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//UNSPECIFIED 就使用默认大小
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// AT_MOST、EXACTLY就使用测量大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected int getSuggestedMinimumWidth() {
//如果view设置了背景,就取背景的最小宽和view最小宽中的最大值
//如果view没有设置背景,就取view的最小值
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
/**
*如果view设置了背景drawable,就返回drawable的原始宽度,否则就返回0
*/
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
UNSPECIFIED:不约束,主要用于listView,scrollview等系统控件中,自定义view一般用不到。
AT_MOST:对应wrap_content,自适应大小。
EXACTLY:精确的,对应match_parent和具体数值。
重点看一下getDefaultSize,当测量模式为UNSPECIFIED时,就使用默认大小,即getSuggestedMinimumWidth中的逻辑,这个我们平时用不到,暂且不看。当测量模式为AT_MOST、EXACTLY时,就使用view的测量大小,可以看到view的宽高是由specSize决定的,如果我们自定义view时直接继承view,而不重写onMeasure()AT_MOST情况下的逻辑,最终view的大小会和match_parent情况一样充满整个父布局,相信很多同学刚开始学习自定义view时都遇到过wrap_content不起作用问题,现在我们找到了问题的关键,就可以对症下药,只需要在AT_MOST时为view设置一个默认值就可以了,对于这个默认大小就要视情况而定了,比如textview的默认大小是里面文字的宽度。
就像这样,当然我们一般不会这么写,而是会重写onMeasure方法,但是思路是一样的。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
// 获取测量模式
int specMode = MeasureSpec.getMode(measureSpec);
// 获取测量大小
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//UNSPECIFIED 就使用默认大小
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// AT_MOST、EXACTLY就使用测量大小
case MeasureSpec.AT_MOST:
result = 合适的默认大小;
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
viewGroup的measure过程
viewGroup的measure过程除了对自己进行measure外,还需要对内部的子view进行measure,viewGroup是一个继承view的抽象类,它为我们提供了一个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);
}
}
}
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);
}
measureChildren()的作用是遍历所有子view,Visible设置为Gone的除外,并根据自己的MeasureSpec和子view的LayoutParams参数通过getChildMeasureSpec()方法确定子view的MeasureSpec,然后将MeasureSpec传递给子view的measure方法进行子view的测量。
为了更好理解viewGroup的measure过程,我们来看一下LinearLayout的onMeasure()方法,以VERTICAL情况为例。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
上面是垂直方向measure的核心代码,宽高是match_parent或具体数值就不多说了,因为这种类型遵循view的宽高测量,主要来看自适应情况下高的处理,可以看到,整个流程大概分为三步,首先遍历所有的子view,获取他们的高度,包括子view的高度和子view垂直方向的margin,用mTotalLength这个变量来记录每次遍历的view的高度,每遍历一次,mTotalLength就增加一次,直到所有的view全部遍历。然后计算layout自己的高度,当然这个高度不能超过父布局的高度。最终调用setMeasuredDimension进行宽高保存。
总结
单一view只需测量自身就可以完成measure,而viewGroup需要依赖子view的宽高来确定自己的测量宽高,但在某些情况下,系统会多次measure来获得最终的测量宽高,所以在layout中获取view的测量宽高是最合适的。