导语
measure是控件工作流程中的测量,是Android很重要的一块内容,他分为对View及ViewGroup的测量,两种测量方式是不一样的,下面通过源码分析View的measure过程。
View的measure过程
View的measure过程是由measure方法来完成,不过该方法是用final关键字修饰的,所以子类不能继承,可以用该方法中的onMeasure方法完成对view的测量工作。
public final void measure(int widthMeasureSpec, int heightMeasureSpec){
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
看onMeasure方法源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
代码很简洁,其中setMeasuredDimension方法设置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;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
MeasureSpec是什么?
它是一个32位的int值,高2位代表specMode,就是通过MeasureSpec.getMode获取,低30位代表specSize 通过MeasureSpec.getSize方法获取,
MeasureSpec如何形成的?
在测量过程中会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec。
specMode有三类:
UNSPECIFIED
父容器不对View有任何限制,一般用于系统内部。
AT_MOST
父容器指定一个可用大小即SpecSize,View的大小不能大于这个值。它对应Layoutparams的wrap_content。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应LayoutParams中的match_parent和具体数值这两种模式。
继续回到源码分析,我们可以看到getDefaultSize返回的大小就是specSize即View测量后大小,这里多次提及测量后大小,是因为View最终大小是在layout阶段确定的,但是几乎所有情况下view测量大小和最终大小是相等的,至于UNSPECIFIED对应的getSuggestedMinimumWidth()这种情况一般用于系统内部测量过程,暂时不做研究。
从getDefaultSize来看,View的宽高是由specSize决定的,但是我们发现在布局中使用wrap_content(对应specMode的AT_MOST)和match_parent(对应specMode的EXACTLY)是等价的,都等于specSize,查View的MeasureSpace创建规则表发现,specSize就是parentSize,而parentSize是父容器中目前可使用的大小。显然在布局中宽/高设置wrap_content系统默认测量是不准确的,那么怎么解决这个问题呢?
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mWidth = 100;
int mHeight = 100;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeight);
} else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, heightSpecSize);
} else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, mHeight);
}
}
在自定义view控件的时候,当specMode检测到时wrap_content的时候设置默认测量值。
ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是ViewGroup是个抽象类,因此它没有重写View的onMeasure方法,但是提供了一个measureChildren的方法。
原理很简单,有兴趣的同学可以自行看源码了解。
有一种情况,在Activity启动的时候就想知道View的宽/高,有的同学可能觉得很简单,在onCreate或者onResumn中去获取View的宽高。实际上在onCreate,onResumn和onStart中均无法得到View的宽高信息,这是因为对View的测量过程和Activity的生命周期是不同步的。那么怎么在界面启动的时候拿到View的测量宽高值呢?
Activity启动时获取View的测量宽高
(1)通过onWindowFocusChanged获取,这个方法的意思是,view加载完了,可以拿取测量宽高了。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
(2)通过post,将runnable投递到消息队列尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
(3)当View树内部发生可见性改变时,addOnGlobalLayoutListener回调会被调用。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observe = view.getViewTreeObserver();
observe.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}