最近做需求需要用到gridview。发现这算得上是官方控件里比较不人性的一个了。为什么这么说呢,它的高度和它内部item的高度设置都不尽如人意。
我们知道gridview里边设置item,还可以设置列数。然后item个数超过列数就会折行,这样就显示成类似表格的样式。
写布局的时候没多想,直接把高度设置成了“wrap_content”。然后写adapter,传数据。运行。可是运行出来发现之显示了一行,第二行以后的item被无情的盖住了。
那么改成“match_parent”,依然不行。除非设成固定高度。
这怎么能忍。可是设置成固定高度又不现实,数据是动态的。
于是首先想到的方法自然是获取item个数,然后计算行数,用行数乘以每行的高度,然后通过layoutParam传给gridview。这自然是一种方案。但是太low了。不喜欢。
我们还是进一步看看GridView的原理,为什么会得出这样一个效果。
直接找到计算高度的onMeasure()方法,由于方法很长,我们直接截取计算高度的部分
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
...
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
int ourSize = mListPadding.top + mListPadding.bottom;
final int numColumns = mNumColumns;
for (int i = 0; i < count; i += numColumns) {
ourSize += childHeight;
if (i + numColumns < count) {
ourSize += mVerticalSpacing;
}
if (ourSize >= heightSize) {
ourSize = heightSize;
break;
}
}
heightSize = ourSize;
}
...
...
setMeasuredDimension(widthSize, heightSize);
}
先大概看一下,就是这个高度值有两种模式,如果是UNSPECIFIED,那么就只有一行的高度,加上上下边距和效果。如果是AT_MOST,那么高度就是所有行的高度,加上上下边距。
显然我们之前显示的效果就是UNSPECIFIED模式的效果。那么我们看看这个到底是什么意思。
这里有一篇文章,讲的比较清楚。
MeasureSpec是一个int值。转换成2进制一共有32位。这32位中,低30位表示高度或宽度的值,而高2位则代表高度或宽度的模式,
一共有三种模式:
1. UNSPECIFIED = 0 << MODE_SHIFT; 即: 00000000 00000000 00000000 00000000 父容器不对子View有任何限制
2. EXACTLY = 1 << MODE_SHIFT; 即: 01000000 00000000 00000000 00000000 父容器已经测量出子View所需要的大小,即measureSpec中封装的specsize
3. AT_MOST = 2 << MODE_SHIFT; 即: 10000000 00000000 00000000 00000000 父窗口限定了一个最大值给子View即specsize
而这三种模式的设置又同时和父VIew和自身设置有关,这张图比较直观:
我们的情况应该是gridview的父view是UNSPECIFIED,所以除非设置具体高度,否则自身的模式都是UNSPECIFIED。为了验证这一点,我自定义了一个View继承GridView并重写了onMeasure方法,分别把高度设置成“wrap_content”和“match_paretn”,输出heightMeasureSpec,结果不出意外,都是0。所以高2位也是00,即UNSPECIFIED的情况。
但是我们也不能随意修改父view,不过既然我们已经重写了onMeasure方法,完全可以自己修改heightMeasureSpec,把他的模式强制改成AT_MOST,代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSpec;
LOGGER.d("centerAAA", "heightMeasureSpec = " + Integer.toBinaryString(heightMeasureSpec));
if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
heightSpec = MeasureSpec.makeMeasureSpec(
0x3fffffff, MeasureSpec.AT_MOST);
}
else {
heightSpec = heightMeasureSpec;
}
super.onMeasure(widthMeasureSpec, heightSpec);
}
这里只修改我们wrap_content的情况,避免影响其他设置。
我们通过MeasureSpec.makeMeasureSpec(0x3fffffff, MeasureSpec.AT_MOST);这个方法修改的。
我们看下这个方法的实现:
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
这个判断是判断的版本,sdk低于17的老版本就用笨方法,直接加,但是这样性能不够好。在17之后采用了与或运算
MODE_SHIFT = 30;
MODE_MASK = 0x3 << MODE_SHIFT;
也就是说,首先通过与运算吧size的高2为置为0,再把mode的低30为置为0,然后两者执行或运算,直接得到结果。
总之最后就是把表示模式的放到高2位,把值放到低30位,拼成一个int值返回。
所以第二个参数我们手动把它设置成AT_MOST。至于第一个参数。我们看到AT_MOST的意思是限制子view的最大值,在这个范围内都能包住。所以我们尽量设置一个最大值,也就是低30位都为1。这样就能成功显示了: