从ListView的嵌套来理解onMeasure方法
文章目录
在Google提供的控件中,有一系列支持触摸滚动的原生控件:ScrollView、ListView、GridView。尽管Google并不推荐我们将这些可滚动的控件进行嵌套使用,但是难免会有业务要求。这时通常会遇到两种问题:
- 当设置内层的控件的高度为match_parent或者wrap_content时,高度不正常
- 触摸事件冲突 对于触摸事件的冲突可以参考我之前对于View中触摸事件的分发的分析来进行解决ANDROID 触摸事件分发源码。这里我们来分析一下为何会出现高度不正常。
先来解决问题
先来看看如何解决ScrollView嵌套ListView的高度问题吧。
通常常用的有两种方法:
- 在ListView设置Adapter之后,获取每一个View,测量每一个View的高度从而得到ListView总的高度,再手动设置
- 重写ListView的
onMeasure
方法
第一种方法我们这里暂且不谈,其实就是通过ListAdapter.getView()
获取相应的View对象再进行测量。相对比较简单,可以去网上自行查找
这里我们来看看第二种方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.Max >> 2,
, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
通过重写onMeasure()
方法后就可以让ListView的高度恢复正常了
onMeasure 源码分析
每个View的绘制过程都会经历measure()
->layout()
->draw()
这样一个流程,在measure()
方法中我们对View的宽高进行测量
/*
* The actual measurement work of a view is performed in onMeasure(int, int), called by this method.
* Therefore, only onMeasure(int, int) can and must be overridden by subclasses.
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 省略
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 省略
}
可以看到measure()
方法是一个final方法,表明子类无法去重写该方法的。在其中调用了onMeasure()
方法,在这个方法中完成实际的测量工作,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure()
方法中调用了setMeasuredDimension()
,从方法名就可以看出来这是将测量后的宽高值设置给当前的View的方法
在getSuggestedMInimumWidth()
中,会判断View是否添加了background。如果没有设置background,会返回android:minWidth属性下设置的值;否则返回的就是android:minWidth和background的较大值
而在getDefaultSize()
中,则是根据父控件传递下来的MeasureSpec和getSuggestedMinimumWidth()
方法返回的值计算出最终的测量值
此外ViewGroup的测量过程是
measureChildren()
->measureChild()
->child.measure()
,依次向下遍历,将所有的子控件进行测量。感兴趣的可以自行查看ViewGroup的源码
MeasureSpec
在onMeasure(int widthMeasureSpec, int heightMeasureSpec)
中我们对View的宽高进行了测量,那么究竟是如何进行测量的呢?从两个参数名可以看出来,肯定是与测量宽高有关的两个int值。
这里就涉及到View的一个静态内部类 MeasureSpec 了,先来看一看源码:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// MeasureSpec的三种模式
public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 想要多大就多大
public static final int EXACTLY = 1 << MODE_SHIFT; // 精确大小
public static final int AT_MOST = 2 << MODE_SHIFT; // 确定最大值
// 通过Mode和Sie去生成MeasureSpec对象
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {<!
return makeMeasureSpec(size, mode);
}
// 获取Mode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 获取Size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
/*
* 省略
*/
}
}
MeasureSpec代表了测量的标准,实际上是一个32位的二进制的值,其中30~31位代表了测量的模式:UNSPECIFIED(0x00)、EXACTLY(0x01)和AT_MOST(0x02),而0~29位封装了测量的大小
此外,在MeasureSpec的makeMeasureSpec()
方法中,通过传入代表模式和大小的值,通过位运算我们就可以得到指定的MeasureSpec对象了
那么每个View在调用onMeasure(int widthMeasureSpec, int heightMeasureSpec)
时,方法中的参数时哪里的来的呢?我们已经知道了,触摸事件的分发是由根节点的ViewGroup依次向下传递,其实View的测量也是这样由根节点依次测量子控件(也就是上面提到的ViewGroup的测量过程),而作为参数传递的MeasureSpec就是由父控件传来的
父控件属性 | 子控件属性 | 子控件的MeasureSpec |
---|---|---|
EXACTLY+size | dp | EXACTLY+dp |
match_parent | EXACTLY+size | |
wrap_content | AT_MOST+size | |
AT_MOST+size | dp | EXACTLY+dp |
match_parent | AT_MOST+size | |
wrap_content | AT_MOST+size | |
UNSPECIFIED+size | dp | EXACTLY+dp |
match_parent | UPSPECIFIED+0 | |
wrap_content | UPSPECIFIED+0 |
所以最终的MeasureSpec是由父控件的测量时得到的MeasureSpec与当前View的属性来决定的。这也是我们之前在LayoutInflater填充XML布局文件时留下的问题,为什么必须要通过父控件才能使当前View的LayoutParams生效,原因也在这里了。
解决高度的原理
回过头来再看看我们一开始解决ListView高度的解决方式,在重写的onMeasure()
方法中,我们通过MeasureSpec.makeMeasureSpec()
方法,手动的生成了一个AT_MOST+size的 MeasureSpec。这样就避免了系统由于两层可滚动的控件嵌套导致的测量错误。当然,这里的size使用的是Integer.MAX >> 2
,获得的值是远远大于屏幕的高度的,所以最终显示的高度还是只有屏幕高度这样
如果我们使用Debug工具在onMeasure()
中获取父控件传递过来的MeasureSpec对象,会发现其实这时候的heightMeasureSpec的值是0,也就是MODE为UNSPECIFIED,size为0。既然是这样为什么还是可以显示ListView中一个element的高度呢?
原来在ListView的onMeasure()
方法中,对最低的高度进行了计算:
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
在不设置padding的情况下,默认得到的高度正好就是一个element的高度。这就是我们不重写默认显示的情况了
文章作者 Dio.Ye
上次更新 2016-02-28