本文主要讲解Android View绘制三大流程(measure,layout,draw)中的measure流程
前言:
为什么需要measure过程?
我们在在绘制UI的时候,基本都是通过XML布局文件的方式来配置UI,而每个View必须要设置的两个群属性就是layout_width和layout_height,这两个属性代表着当前View的尺寸。
所以这两个属性的值是必须要指定的,这两个属性的取值只能为三种类型:
1、固定的大小,比如100dp
2、刚好包裹其中的内容,wrap_content
3、想要和父布局一样大,match_parent
由于Android希望提供一个更优雅的GUI框架,所以提供了自适应的尺寸,也就是 wrap_content 和 match_parent 。
试想一下,那如果这些属性只允许设置固定的大小,那么每个View的尺寸在绘制的时候就已经确定了,所以可能都不需要measure过程。但是由于需要满足自适应尺寸的机制,所以需要一个measure过程。
measure
的概念:
measure
为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight
)和宽(对应属性:mMeasureWidth
),每个View的控件的实际宽高都是由父视图和本身视图决定的。
如何合理的测量一颗View树?
因为measure过程最终是要确定每个View的实际大小,也就是准确的像素值。但是刚开始的时候,View中layout_width和layout_height两个属性的值,都只是自适应的尺寸,也就是match_parent和wrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值 。所以当一个View需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。
1、针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。
2、针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。 但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸(例如TextView的单行长度大于父View的话,那么他会进行换行处理)。
下面一个ViewGroup和2个View的简单场景。大概示意图如下:
自定义View的测量,需要重写onMeasure()方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
// super
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
MeasureSpec
关键字
1.如何理解MeasureSpec
(测量规格)?
这个MeasureSpec 封装的是父容器传递给子容器的布局要求,而不是父容器对子容器的布局要求,“传递” 两个字很重要
更严谨的说法应该是这个MeasureSpec是由父View的MeasureSpec和子View(自身)的LayoutParams通过简单的计算得出一个针对子View的测量要求 ,这个测量要求就是MeasureSpec。
2.MeasureSpec
的三种模式:
UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大(不限制)
EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间(限制固定值)
AT_MOST:子容器可以是声明大小内的任意大小(限制上限)
3.MeasureSpec
和LayoutParams
的联系:
在第一点已经提到MeasureSpec是由父View的MeasureSpec和子View(自身)的LayoutParams通过简单的计算得出一个针对子View的测量要求
然后从代码上来看view.measure(int widthMeasureSpec, int heightMeasureSpec) 的两个MeasureSpec是父类传递过来的,但并不是完全是父View的要求 ,而是父View的MeasureSpec和子View自己的LayoutParams共同决定的,而子View的LayoutParams其实就是我们在xml写的时候设置的layout_width和layout_height 转化而来的
下面通过查看ViewGroup
的原码理解这两者之间的联系:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 子View的LayoutParams 主要是用到xml中的layout_width和layout_height
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 获取子View的MeasureSpec
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);
// 用子View的MeasureSpec(一个测量要求,比如不能超过多大)去测量自己,如果子View是ViewGroup 那还会递归往下测量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// measure方式是用final修饰的,如果自定义View需要使用测量,那么应该重写onMeasure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); // 获取父View(当前)的Mode
int specSize = MeasureSpec.getSize(spec); // 获取父View(当前)的大小
int size = Math.max(0, specSize - padding); // 获取子View可用的实际大小
int resultSize = 0;
int resultMode = 0;
// LayoutParams.MATCH_PARENT = -1
// LayoutParams.WRAP_CONTENT = -2
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
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;// 暂时无法获取子View确切大小,前提是不能比父View大,默认设置和父View一样大
resultMode = MeasureSpec.AT_MOST;
}
break;
/**
* 总结:
* 如果父View的MeasureSpec 是EXACTLY,说明父View的大小是确切的,
* 确切的意思很好理解,如果一个View的MeasureSpec 是EXACTLY,那么它的size是多大,最后展示到屏幕就一定是那么大。
*/
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
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;
/**
* 总结:
* 如果父View的MeasureSpec 是AT_MOST,说明父View的大小是不确定,最大的大小是MeasureSpec 的size值,不能超过这个值。
*/
// 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;
/**
* 总结:
* 如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示没有任何束缚和约束,不像AT_MOST表示最大只能多大,
* 不也像EXACTLY表示父View确定的大小,子View可以得到任意想要的大小,不受约束
*
* View.sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M
* 6.0以下的一些列表嵌套需要手动计算高度(猜的)
*/
}
//根据上面获取的mode和size构建MeasureSpec对象。
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
提出疑问!
按照上述的逻辑 有没有可能子View比父View大?
android:clipChildren = "false"
什么情况下产生MeasureSpec.UNSPECIFIED?
适用于会滚动的View
ViewGroup的测量过程:
ViewGroup是一个抽象类它并没有实现onMeasure,我们知道测量过程其实都是在onMeasure方法里面做的,我们来看下FrameLayout 的onMeasure 方法,具体分析
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
}
// setMeasuredDimension()
}
大致描述一下测量步骤:
1.测量(绘制)入口描述:
既然我们知道整个View的Root是DecorView,那么View的绘制是从哪里开始的呢,我们知道每个Activity 均会创建一个 PhoneWindow对象,是Activity和整个View系统交互的接口 ,每个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,对于Activity来说,ViewRootImpl是连接WindowManager和DecorView的纽带, 绘制的入口是由ViewRootImpl的performTraversals方法来发起Measure,Layout,Draw等流程
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}