前言
上篇博客主要介绍了我们了解View工作原理前应该掌握的三个基础概念:ViewRoot,DecorVIew,MeasureSpec。这之后几篇博客主要介绍View的三大流程(Measure ,layout,draw),这篇博客主要从源码的角度来解析View及ViewGroup的Measure(测量流程)。前面我们介绍过Measure过程的主要工作是确定View的测量宽/高。
View的Measure过程
View的measure过程是由一个final类型的方法measure()来完成的,这个方法回去调用onMeasure()方法;我们自定义View也是重写onMeasure()方法。
View.onMeasure
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
//setMeasuredDimension设置View宽/高的测量值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),
widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),
heightMeasureSpec));
}
我们可以看到该方法里面调用了setMeasureDimension(),这个方法主要是用来设置View的宽/高的测量值。我们主要看在这个方法中调用的getDefaultSize()方法:
public static int getDefaultSize(int size, int measureSpec){
int reasult = size ;
int specMode = MeasureSpec.getMode(measureSpec);//得到测量模式
int specSize = MeasureSpec.getSize(measureSpec);//得到在该模式下的尺寸
switch(specMode){//根据测量模式返回size
case MeasureSpec.UNSPECIFIED://该模式通常用于系统内部测量我们不需要去看
reasult = size;
break;
case MeasureSpec.AT_MOSTT:
case MeasureSpec.EXACTLY:
reasult = specSize;//这两种情况均返回View测量后的大小
break;
}
return result ;
}
这段代码的逻辑非常简单,就是返回View的测量后的大小,这个大小基本也是View的最终大小,View的最终大小是在layout阶段确定的。下面我们来看下getDefaultSize()中调用的getSUggestedMinimuHeight()和 getSUggestedMinimuWidth()方法:
protected int getSuggestedMinimumWidth(){
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimuWidth());
}
protected int getSuggestedMinimumHeight(){
return (mBackground == null) ? mMinHeight : max(mMinHeight,mBackground.getMinimuHeight());
}
我们看到这两个方法非常简单,实现原理也是一样的:就是看View有没有设置背景 ,如果没有设置则View的宽/高为mMinWidth/mMinHeight,如果设置了背景,则View的宽/高为:mMinWidth ~mBackground.getMinmumWidth() / mMinHeight ~mBackground.getMinmumHeight() 两者中较大的。
*注:mMinWidth / mMinheight :View的android:minWidth/minHeight属性,如果没指定则默认为0.
mBackground.getMinmumWidth() /getMinmumHeight() 返回背景的原始宽度,如果未设置则返回0.
我们可以这样理解:getSUggestedMinimuHeight()和 getSUggestedMinimuWidth()方法返回的其实就是View在UNSPECIFIED模式下的测量的宽/高。
从getDefaultSize()方法我们我们知道View的测量大小由SpecSize决定,所以我们在自定义View后要在onMeasure方法中设置wrap_content的大小,不然就和match_parent一样。这个现象的原因我们可以结合上一篇博客中的最末尾的表格来理解。如果View在布局中使用wrap_content,那么它的是specMode是AT_MOST模式,在这种模式下它的宽/高为specSize,由表格知这种情况下View的specSize是parentSize,也就是父容器当前剩余空间的大小。
ViewGroup的measure流程
对于ViewGroup来说,除了完成自己的measure过程外,还会遍历去调用它的所有子元素的Measure方法,各个子元素再递归去执行这个过程。View不同于ViewGroup,ViewGroup没有onMeasure方法,但是提供了一个measureChildren()方法。这个方法对于每个子元素会遍历去调用measureChild方法,这样递归去调用的子元素,直到调用的子元素不是ViewGroup而是View。就去调用View的onMeasure。
这里可能有人会有和我同样觉得好奇,为什么ViewGroup没有onMeasure方法昵?
不同的ViewGroup子类的布局特性是不一样的,比如 A 是 LinerLayout,B 是 RelativeLayout。它们的布局特性使得它们的测量细节是不一样的,所以我们没办法在ViewGroup去统一测量。
在渲染前获取 View 的宽高
这是一个比较有意义的问题,或者说有难度的问题,问题的背景为:有时候我们需要在view渲染前去获取其宽高,典型的情形是,我们想在onCreate、onStart、onResume中去获取view的宽高。如果大家尝试过,会发现,这个时候view还没有measure好,宽高都为0,那到底该怎么做才能正确获取其宽高呢,下面给出三种方法:
- Activity/View#onWindowFocusChanged :这个方法表明,view已经初始化完毕了,宽高已经准备好了
- view.post(runnable) :通过post可以将一个runnable投递到消息队列的尾部,然后等待looper调用此runnable的时候,view也已经初始化好了
- view.measure(int widthMeasureSpec, int heightMeasureSpec) :通过手动去measure来视图得到view的宽高
前两种方法都比较好理解也比较简单,这里主要介绍下第三种方法的详细用法:
采用 view.measure 去提前获取 view 的宽高,根据 view 的 layoutParams 来分
match_parent
直接放弃,无法 measure 出具体的宽高
具体的数值( dp/px )
比如宽高都是 100px ,如下 measure :
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
wrap_content
如下 measure :
int widthMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
注意到(1 << 30) - 1,通过分析MeasureSpec的实现可以知道,view的尺寸使用30位二进制表示的,也就是说最大是30个1即 2^30 - 1,也就是(1 << 30) - 1,在最大化模式下,我们用view理论上能支持的最大值去构造MeasureSpec是合理的。
关于view的measure,网络上有两个错误的用法,如下,为什么说是错误的,首先违背了系统的内部实现规范(因为无法通过错误的MeasureSpec去得出合法的SpecMode从而导致measure出错),其次不能保证一定能 measure 出正确的结果。
第一种错误用法
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);
第二种错误用法
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
参考资料
《Android开发艺术与探索》第四章View的工作原理;任玉刚;