前言:前几天把onMeasure,onLayout的相关方法学习了一下,打算在做一个项目有深刻理解以后,在总结自己关于这两个方法的学习。学习当中,感觉又会遇到新的问题,新的要学习的知识点。感觉如果还不把之前学习的内容,记录下来,现在不抽出时间,那么以后也抽不出时间。所以,学习一个知识点,就要记录下,不要拖。或许,这就是解决拖延症的办法:做一件事就做彻底,不留尾巴。Just Do It.
首先明确一点,关于View的视图结构,是一个树形递归结构。不论我们是在执行onMeasure方法,还是onLayout方法,都是按照这个流程结构走的。无论我们测量、还是调整布局,都是从树的顶端,一层一层遍历,一个分支一个分支的执行measure,onMeasure,都测量完毕,在递归执行layout,onLayout。
所学onMeasure知识,关于对其总结,也是基于学习了各个博主的讲解,结合源码。最后给出相关博主连接。
Android 应用程序UI绘制分为三个过程(先后顺序是):测量measure,布局位置layout,绘制draw。
关于View的递归遍历,不仅是onMeasure适用,onLayout也一样适用。
一次遍历下来,第一个子控件以及这个子控件中的所有子控件都会完成测量工作;然后开始测量第二个子控件…;
最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,
这个过程不只执行一次,也就是说有可能重复执行,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,
就会重新测量一遍。上图:
一、在自定义View中,一般情况下,只有View的宽、高属性需要wrap_content的时候,才会用到onMeasure方法。
在View的源码中,measure(w,h)方法测量View的实际大小。是个final类型,不允许外部修改调用。所以继承View的扩展类,只能调用measure(w,h)里的onMeasure()方法,对布局进行测量。但是子View计算自己宽高时,可以调用该方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
看onMeasure()方法内部:
/**
* 这个方法需要被重写,应该由子类去决定测量的宽高值.onMeasure方法宽高得到的是父布局控件的宽高
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在onMeasure()方法内部调用了setMeasuredDimension()方法。,作用是来存储测量宽,高值。作用是来存储测量宽,高值。作用是来存储测量宽,高值。(重要说三遍。)
/**
* 这个方法必须由onMeasure(int, int)来调用,来存储测量的宽,高值。
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
在返回onMeasure()方法内,getDefaultSize()方法。看内部方法:
/**
* 作用是返回一个默认的值,如果MeasureSpec没有强制限制的话则使用提供的大小.否则在允许范围内可任意指定大小
* 第一个参数size为提供的默认大小,第二个参数为测量的大小
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);//取mode
int specSize = MeasureSpec.getSize(measureSpec);//取size
switch (specMode) {
// Mode = UNSPECIFIED时使用提供的默认大小
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// Mode = AT_MOST,EXACTLY时使用测量的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以看出,如果是mode为:MeasureSpec.UNSPECIFIED时,调用取传递进来默认值。剩余两种情况,则是根据传递进来的measureSpec来取size大小。
在getDefaultSize方法里传参measureSpec,里边封装了有我们需要去取两个东西,一个是mode,一个是size。
mode有三个值:UNSPECIFIED、EXACTLY、AT_MOST。在我们布局里,就是分别是:指定具体宽高值,match_parent,wrap_content.三种方式。
size就是指的宽、高。
而measureSpec、size、mode他们三个关系,都封装在View的一个内部类,MeasureSpec类当中。
来看下MeasureSpec类当中方法:
/**
* MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求
* MeasureSpec由size和mode组成。
* 三种Mode:
* 1.UNSPECIFIED
* 父不没有对子施加任何约束,子可以是任意大小(也就是未指定)
* (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,模式为UNSPECIFIED
* 2.EXACTLY
* 父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。
* (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)
* 3.AT_MOST
* 子最大可以达到的指定大小
* (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)
*
* MeasureSpecs使用了二进制去减少对象的分配。
*/
public class MeasureSpec{
// 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位)
private static final int MODE_SHIFT = 30;
// 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
// (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 0向左进位30,就是00 00000000000(00后跟30个0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 1向左进位30,就是01 00000000000(01后跟30个0)
public static final int EXACTLY = 1 << MODE_SHIFT;
// 2向左进位30,就是10 00000000000(10后跟30个0)
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* 根据提供的size和mode得到一个详细的测量结果
*/
// measureSpec = size + mode; (注意:二进制的加法,不是十进制的加法!)
// 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
// 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
/**
* 通过详细测量结果获得mode
*/
// mode = measureSpec & MODE_MASK;
// MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
// 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* 通过详细测量结果获得size
*/
// size = measureSpec & ~MODE_MASK;
// 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
/**
* 重写的toString方法,打印mode和size的信息,这里省略
*/
public static String toString(int measureSpec) {
return null;
}
}
注意的是mode与size 的关系,二者是通过integer32位二进制来表示的。最前两位表示mode值,后三十位表示size大小。
我们在重写onMeasure时候,需要用到测量宽、高的精确值可以用到MeasureSpec.makeMeasureSpec(size,mode),返回一个int measureSpecWidth,int measureSpecHeight.
获取size、mode大小就是通过该内部类静态方法
int size = MeasureSpec.getSize(int measureSpecWidth);//或者传参 measureSpecHeight
int mode = MeasureSpec.getMode(int measureSpecWidth);//或者传参 measureSpecHeight
二、在ViewGroup、或者多个ViewGroup中,就用到了视图树的递归调用。
在测量之前首先要明确一点,需要测量的是一个View,还是一个ViewGroup,还是多个ViewGroup。如果是一个View测量一个就可以了。如果是ViewGroup或者多个ViewGroup嵌套,我们就需要循环遍历视图中所有的View。遍历顺序按着遍历完一个二叉树分支,在遍历另一个分支...
<com.lhq.fight.CostomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffefff"
>
<TextView
android:text="@string/tv_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#5dff00ff"
android:id="@+id/textView1" />
</com.gxy.text.CostomViewGroup>
在自定义View中嵌套childView。在onMeasure中测量childView方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//调用ViewGroup类中测量子类的方法
measureChildren(widthMeasureSpec, heightMeasureSpec);
//调用View类中默认的测量方法
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
super.onMeasure()测量值是父布局的详细宽高。而measureChildren()方法,是测量当前ViewGroup中childView的方法。
几个关于测量childView时,有用的API:
int count = getChildCount();//获取当前ViewGroup中所有ChildView的个数。
int childView = getChildAt(index);//根据下标或者指定的childView。在遍历视图树时候使用。
int realWidth = getMeasuredWidth();//测量得到View的真实宽、高
int realHeight = getMeasuredHeight();
/**
* 遍历所有的子view去测量自己(跳过GONE类型View)
* @param widthMeasureSpec 父视图的宽详细测量值
* @param heightMeasureSpec 父视图的高详细测量值
*/
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);
}
}
}
在measureChidlren方法中,如果当前子View的状态不是View.GONE状态,则遍历每一个子View后,都会测量每一个子View的宽高。
/**
* 测量单个视图,将宽高和padding加在一起后交给getChildMeasureSpec去获得最终的测量值
* @param child 需要测量的子视图
* @param parentWidthMeasureSpec 父视图的宽详细测量值
* @param parentHeightMeasureSpec 父视图的高详细测量值
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 取得子视图的布局参数
final LayoutParams lp = child.getLayoutParams();
// 通过getChildMeasureSpec获取最终的宽高详细测量值
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 将计算好的宽高详细测量值传入measure方法,完成最后的测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
还有一个在计算子View时,需要计算View的边距:
measureChild 和measureChildWithMargins的区别就是是否把margin作为子视图的大小。需要计算进去。
//widthUsed、heightUsed就是传入的额外使用空间,margin的距离。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
最后childView去调用 measure方法。我们在最后measure 我们的childView时,需要传入childView的详细宽、高值。那么关于宽、高值得计算是在getChildMeasureSpec方法中
来看代码:
/**
* 在measureChildren中最难的部分:找出传递给child的MeasureSpec。
* 目的是结合父view的MeasureSpec与子view的LayoutParams信息去找到最好的结果
* (也就是说子view的确切大小由两方面共同决定:1.父view的MeasureSpec 2.子view的LayoutParams属性)
*
* @param spec 父view的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension child在当前尺寸下的布局参数宽高值(LayoutParam.width,height)
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父view的模式和大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
int size = Math.max(0, specSize - padding);
//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;
//通过1.父view的MeasureSpec 2.子view的LayoutParams属性这两点来确定子view的大小
switch (specMode) {
// 当父view的模式为EXACITY时,父view强加给子view确切的值(一般是父view设置为match_parent或者固定值的ViewGroup)
case MeasureSpec.EXACTLY:
// 当子view的LayoutParams>0也就是有确切的值
if (childDimension >= 0) {
//子view大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为MATCH_PARENT时(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为WRAP_CONTENT时(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大(多见于ListView、GridView)
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这段代码,就是说明父View的mode最终来限定子View(childView)的mode,与size。通过size,与mode,最终计算出当前childView的详细宽、高。
在LayoutParams中
FILL_PARENT = -1;//方法过时
MATCH_PARENT = -1;
WRAP_CONTENT = -2;
最后通过一系列条件判断,计算出size,mode,来返回子View的详细宽、高值。MeasureSpec.makeMeasureSpec(resultSize,resultMode);
仔细看,可以用一个表格来概括这一段代码。
最后给出学习参考:
最后感谢各位博主的分享知识,感谢。