上一篇博客介绍了我们自己写的布局是如何通过setContentView()来加载到屏幕中。这一篇将会继续讲解当布局文件加载出来后,布局里边的控件是经过怎样的步骤来显示出来的。
在上一篇博客中最后分析出我们自己写的布局最终会加载到DecorView中id为content的FramLayout中,而Framlayout继承自ViewGroup。所以如果要分析整个控件的绘制流程首先要从ViewGroup着手。
我们平时写布局应该对ViewGroup非常了解。ViewGroup相当于一个容器,里边可以放ViewGroup或者View对象。而ViewGroup和View的包含区别就是ViewGroup里边可以添加,View不可以在添加。
了解了ViewGroup和View的关系后,那么ViewGroup里的ViewGroup和View是经过怎样的过程最终显示到设定的位置的呢?其实ViewGroup就相当于一棵树,View相当于树的叶子。ViewGroup要经历 测量onMeasure(),摆放onLayout(),画 onDraw()三个主要方法最终显示出来。
首先来看onMeasure()测量的方法。
首先我们提出一个猜想,在整个的ViewGroup的测量步骤中,首先要测量并确定所有的子View的大小,再确定View树的根ViewGroup的大小,就像数据结构中的树的前序遍历。
根据前面的猜想,我们应该先测量所有的子View。在ViewGroup方法的中,我们看到了measureChildren()方法,所以可以根据这个方法作为线索,向下分析。
1 measureChildren() 测量所有子View
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//1 遍历子View
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//2 测量子View
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
这个方法有两个参数,widthMeasureSpec和heightMeasureSpec。所以在看这个方法之前,先来看一下MeasureSpec。
MeasureSpeck用32位的int值来表示,高两位表示MeasureSpec的SpecMode属性,低30位表示SpecSize属性。
SpecMode有三种:
- EXACTLY 精确值 当宽高设置具体的精确值 比如200dp。
- AT_MOST 最大值 一般用于当设置宽高为martch_parent或者wrap_content时。
- UNSPECIFIED 未指定的大小,一般用于ListView,ScrollView等不能确定大小的控件。
SpecSize就表示控件的大小的值。
SpecMode和SpecSize可以通过MeasureSpec的getSpecMode()和getSpecSize()方法来获取。
同时,也可以把SpecMode和SpecSize通过MeasureSpec的makeMeasureSpec()方法来合成一个MeasureSpec。
下面再来看上面这段代码,从这个方法中可以看出,首先要遍历所有的子View,再调用measureChild()方法测量子View。下面来看测量View自己的方法。
2 measureChild() 子View测量自己
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//1 获取child自己的相关参数
final LayoutParams lp = child.getLayoutParams();
//2 获取childWidthMeasureSpec 和 childHeightMeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//3 根据宽高属性,测量自己
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
1 这个方法中首先获取了child自己的参数Layoutparams。
2 然后根据所在ViewGroup的MeasureSpec和padding,本身的宽度来获取宽和高的MeasureSpec。
3 根据宽和高的MeasureSpec来测量自己。
下面来看ViewGroup中测量的核心:
2.1 getChildMeasureSpec() 获得子View的MeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//1 获得父ViewGroup的specMode和specSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//2判断父ViewGroup的specMode
switch (specMode) {
//2.1如果父ViewGroup是一个具体的值
case MeasureSpec.EXACTLY:
//2.1.1 child是一个具体值
if (childDimension >= 0) {
//child的size将会是child自己设定的值
resultSize = childDimension;
//因为child的size是精确值,所以child的specMode是EXACTLY
resultMode = MeasureSpec.EXACTLY;
//2.1.2 child想和父ViewGroup一样大
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因为此时父ViewGroup的size是确定的,child又想和父ViewGroup一样大,所以child的size和父ViewGroup的size相等
resultSize = size;
//因为child的size是精确值,所以child的specMode是EXACTLY
resultMode = MeasureSpec.EXACTLY;
//2.3.3 child想要自己本身的大小
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//限制child不能比父ViewGroup大 让child的size和父ViewGroup的size相等
resultSize = size;
//child大小此时不能确定 所以specMode也是AT_MOST
resultMode = MeasureSpec.AT_MOST;
}
break;
//2.2如果父ViewGroup是一个最大值
case MeasureSpec.AT_MOST:
//2.2.1 child是一个具体值
if (childDimension >= 0) {
//child的size将会是child自己设定的值
resultSize = childDimension;
//因为child的size是精确值,所以child的specMode是EXACTLY
resultMode = MeasureSpec.EXACTLY;
//2.3.2 child想和父ViewGroup一样大
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//child想和父ViewGroup一样大,但是父ViewGroup的size是不固定的,所以限制child不能比父ViewGroup大
resultSize = size;
// child的specMode也是AT_MOST
resultMode = MeasureSpec.AT_MOST;
//2.3.3 child想要自己本身的大小
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
//同样限制child不能比父ViewGroup大
resultSize = size;
//同样child的specMode也是AT_MOST
resultMode = MeasureSpec.AT_MOST;
}
break;
//2.3如果父ViewGroup想知道子控件有多大
case MeasureSpec.UNSPECIFIED:
//2.3.1 child是一个具体值
if (childDimension >= 0) {
//child的size将会是child自己设定的值
resultSize = childDimension;
//因为child的size是精确值,所以child的specMode是EXACTLY
resultMode = MeasureSpec.EXACTLY;
//2.3.2 child想和父ViewGroup一样大
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//child的size将会是0或者父ViewGroup的大小
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
//因为child的父ViewGroup是未指定的,child又想和父ViewGroup一样大,所以child的specMode也是UNSPECIFIED
resultMode = MeasureSpec.UNSPECIFIED;
//2.3.3 child想要自己本身的大小
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//child的size将会是0或者父ViewGroup的大小
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
// 同时child的specMode也是UNSPECIFIED
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//3 把child的SpecSize和SpecMode合成一个MeasureSpec并返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这段代码可以算是ViewGroup中的重点代码了。我几乎把所有的代码都写了注释。这个方法主要是结合ViewGroup的MeasureSpec和child本身的大小来确定child的MeasureSpec。具体是如何影响child的MeasureSpec的,可以看我写的注释。写的非常清楚,这里不再赘述。
获得了child的MeasureSpec后,开始调用
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
而measure()方法中的测量工作主要是在onMeasure()中来完成了的。来看下面这段伪代码:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
再来看onMeasure()方法:(注意这个是View中的方法,当然了,ViewGroup也是继承自View的,但是这里指的是View中的方法)
3 onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在onMeasure()方法中,
1 首先获得推荐的最小宽度或高度。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
2 根据推荐的高度和宽度,在结合child的MeasureSpec。来确定child的默认宽高。
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;
}
3最后通过setMeasuredDimension()方法来把存储View的宽和高
4 setMeasuredDimension
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(measuredWidth, measuredHeight);
}
存储真正的操作是在setMeasuredDimensionRaw()方法中,在setMeasuredDimensionRaw()方法中直接把宽和高保存在成员变量中。
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
至此,View的宽和高已经确定。测量过成完成。但是这只是ViewGroup其中的一个子View的过程。如果ViewGroup1的子View也是一个ViewGroup2。那么最先测量的应该是ViewGroup2的子View。这是个递归的过程,就像是树的遍历。先遍历最小最深的结点。
需要注意的是,如果ViewGroup在测量过程中,子View也是一个ViewGroup。那么是怎么实现递归的呢?如FramLayout是怎么再去测量呢?其实FrameLayout重写了onMeasure()方法,在此方法中,还是要先去遍历所有的子View,然后再测量。如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
//1 遍历每个child
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//2 开始测量每个child
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());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
这是FramLayout中的onMeasure()方法的一部分,可以看出先去遍历,然后再去测量每个childView。