前言
一个界面就像一张报纸,如果你想让他合理的排版,首先需要测量每个板块(子view)的大小,然后根据大小布局合适的位置,最后在绘制内容。
测量阶段:从上到下递归地调用每个 View 或者 ViewGroup 的 measure() 方法,测量他们的尺寸并计算它们的位置;
View 或 ViewGroup 的测量过程
测量阶段,measure() 方法被父 View 调用,在 measure() 中做一些准备和优化工作后,调用 onMeasure() 来进行实际的自我测量。 onMeasure() 做的事,View 和 ViewGroup 不一样:
-
View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;
-
ViewGroup:ViewGroup 在 onMeasure() 中会调用所有子 View 的 measure() 让它们进行自我测量,并根据子 View 计算出的期望尺寸来计算出它们的实际尺寸和位置(实际上 99.99% 的父 View 都会使用子 View 给出的期望尺寸来作为实际尺寸,原因在下期或下下期会讲到)然后保存。同时,它也会根据子 View 的尺寸和位置来计算出自己的尺寸然后保存;
测量View
1.重写 onMeasure() 来修改已有的 View 的尺寸;
具体步骤:
- 重写 onMeasure() 方法,并在里面调用 super.onMeasure(),触发原有的自我测量;
- 在 super.onMeasure() 的下面用 getMeasuredWidth() 和 getMeasuredHeight() 来获取到之前的测量结果,并使用自己的算法,根据测量结果计算出新的结果;
- 调用 setMeasuredDimension() 来保存新的结果。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//1.触发原有的自我测量
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//2.1获取原有测量结果
var resultWidth = measuredWidth
var resultHeight = measuredHeight
"start resultWidth $resultWidth resultHeight $resultHeight ".e()
//2.3重新计算宽高
if (resultWidth > resultHeight){
resultWidth = resultHeight
}else{
resultHeight = resultWidth
}
"end resultWidth $resultWidth resultHeight $resultHeight ".e()
//3.保存新的结果
setMeasuredDimension(resultWidth,resultHeight)
}
2.重写 onMeasure() 来全新定制自定义 View 的尺寸
全新定制尺寸和修改尺寸的最重要区别
需要在计算的同时,保证计算结果满足父 View 给出的的尺寸限制
父 View 的尺寸限制
- 由来:开发者的要求(布局文件中 layout_ 打头的属性)经过父 View 处理计算后的更精确的要求;
- 限制的分类:
UNSPECIFIED:不限制
AT_MOST:限制上限
EXACTLY:限制固定值
具体步骤:
- 重新 onMeasure(),并计算出 View 的尺寸;
- 使用 resolveSize() 来让子 View 的计算结果符合父 View 的限制(当然,如果你想用自己的方式来满足父 View 的限制也行)。
resolveSize最终调用源码
//size View测量自身的大小
//measureSpec 父View的限制
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
//限制上限
case MeasureSpec.AT_MOST:
//如果大于上限,返回上限大小,否返回view自身测量的大小
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
//父设置了精确值,就得按照精确值大小
case MeasureSpec.EXACTLY:
result = specSize;
break;
//父没有设置,想多大就多大,就得按照自身大小
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
使用时,只需要将自身测量的大小和父view的限制传给resolveSize即可
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var resultWidth = 200
var resultHeight = 300
resultWidth = resolveSize(resultWidth,widthMeasureSpec)
resultHeight = resolveSize(resultHeight,heightMeasureSpec)
setMeasuredDimension(resultWidth,resultHeight)
}
MeasureSpec是怎么来的?
继续往下看
不按照父限制会有什么后果?
会出bug,比如你在布局设置了match_parent,由于你测量的结果是400dp,而父view剩余空间有500dp,就会出现空隙等问题。所以要满足父view的限制。
重写 onMeasure() 测量ViewGroup
具体步骤
- 调用每个子 View 的 measure() 来计算子 View 的尺寸
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
for (i in 0 until childCount){
val childView = getChildAt(i)
childView.measure(childWidthSpec,childHeightSpec)
}
}
重点来了childWidthSpec、childHeightSpec是父View对子View的尺寸限制,这两个并不是现成的,那怎么来的呢?
这个限制是根据开发者的需求即xml中layout_开头的配置和ViewGroup的剩余空间,结合起来计算得到的。这里要注意开发者的要求在地位上是绝对高于剩余空间的。例如开发者写了layout_width = "48dp",那么就不用去计算了,直接以开发者的要求48dp作为测量的大小
怎么获取xml中layout_width、layout_height?
for (i in 0 until childCount){
val childView = getChildAt(i)
val lp = childView.layoutParams
// lp.width --> layout_width
// lp.height --> layout_height
}
ViewGroup的可用空间则么算?
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
通过参数widthMeasureSpec,heightMeasureSpec可以获取自己的可用空间。不过这里也分集中情况。
widthMeasureSpec 封装了父View对子View的尺寸限制,这里的限制分为三种情况。如果是EXACTLY、AT_MOST 那么剩余空间就是widthMeasureSpec中的size,如果是UNSPECIFIED 那么剩余空间就是无限大。
when (modeWidth) {
MeasureSpec.EXACTLY,
MeasureSpec.AT_MOST -> {
剩余空间 = modeWidth
}
MeasureSpec.UNSPECIFIED -> {
剩余空间 = 无限大
}
}
得到了开发者的要求和父view的剩余空间,来给子view设置MeasureSpec尺寸限制。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val modeWidth = MeasureSpec.getMode(widthMeasureSpec)
val sizeWidth = MeasureSpec.getSize(widthMeasureSpec)
val modeheight = MeasureSpec.getMode(heightMeasureSpec)
val sizeheight = MeasureSpec.getSize(heightMeasureSpec)
for (i in 0 until childCount) {
val childView = getChildAt(i)
val lp = childView.layoutParams
var childWidthSpec: Int
when (lp.width) {
MATCH_PARENT ->{
//沾满父View剩余空间,剩余空间根据widthMeasureSpec
if (modeWidth == MeasureSpec.EXACTLY || modeWidth == MeasureSpec.AT_MOST){
childWidthSpec = MeasureSpec.makeMeasureSpec(sizeWidth - usedWidth, MeasureSpec.EXACTLY)
}else{
childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
}
}
WRAP_CONTENT ->{
//沾满父View剩余空间,剩余空间根据widthMeasureSpec
if (modeWidth == MeasureSpec.EXACTLY || modeWidth == MeasureSpec.AT_MOST){
childWidthSpec = MeasureSpec.makeMeasureSpec(sizeWidth - usedWidth, MeasureSpec.AT_MOST)
}else{
childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
}
}
else -> {
//精确值 如48dp
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY)
}
}
}
}
- 计算子 View 的位置并保存子 View 的位置和尺寸
- 计算自己的尺寸并用 setMeasuredDimension() 保存
参考资料 扔物线