简介
在我们开发项目过程中,我们在座绝大部分的人一定都遇到过ScrollVIew嵌套ListView显示不全,只显示一个item高度的问题,当然,你如果说你没有遇到过也是没有问题的。这种现象是很常见的,网上一搜基本解决方法一般有这样几种,
1.1 动态设置listview,去测量每个item高度,通过for循环叠加计算listview的总高度
1.2 使用LinearLayout代替ListView
1.3 自定义MyListView直接集成系统的ListView,重写onMeasure()方法
等等...-
实现
接下来带大家具体分析下这几种方式的具体使用,前两种使我们这节课的一个小插曲,待会看下其实现方式即可,今天我们重点带大家看下第三种实现方式,它为什么要这样去写方式一: 动态设置listview,去测量每个item高度,通过for循环叠加计算listview的总高度
我们大家都知道,在布局文件中如果直接将item高度写死,是可以解决这个问题的,但是我们也都知道,listview高度随着数据是可变化,实际高度还需要实际去测量,那么既然这样,我们就可以手动的去计算ListView的高度了,代码如下,直接作为工具类拷贝到项目中即可使用
/**
* Describe: 动态设置listview,去测量每个item高度,通过for循环叠加计算listview的总高度
*
* Author: Jack-Chen
*
* Time 16/9/27 下午4:28
*/
public class ListViewUtil {
public static void adaptiveHight(Context context,ListView listView,float dividerHeight) {
try {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return;
}
int totalHeight = 0;
for (int i = 0; i < listAdapter.getCount(); i++) {
View listItem = listAdapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
if (dividerHeight != -1) {
totalHeight += UIHelper.dip2px(context, dividerHeight) * (listAdapter.getCount() - 1);
}
params.height = totalHeight;
listView.setLayoutParams(params);
}catch (Exception ex){
ex.printStackTrace();
}
}
public static int getItemsHight(ListView listView) {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return 0;
}
int totalHeight = 0;
for (int i = 0; i < listAdapter.getCount(); i++) {
View listItem = listAdapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
return totalHeight;
}
public static int getItemHight(ListView listView) {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return 0;
}
int itemHeight = 0;
if(listAdapter.getCount()>0) {
View listItem = listAdapter.getView(0, null, listView);
listItem.measure(0, 0);
itemHeight= listItem.getMeasuredHeight();
}
return itemHeight;
}
}
方式二: 使用LinearLayout代替ListView
既然listview不能适应ScrollView,那么我们完全可以找一个可以适应ScrollView的控件来代替ListView,此时LinearLayout是最好的选择,但如果我们还想继续使用已经定义好的adapter,那么我们只需要定义一个类去继承LinearLayout,最后为其适配BaseAdapter即可
具体代码如下:
2.2.1: 自定义LinearLayoutForListView 继承LinearLayout
/**
* Describe: 使用LinearLayout代替ListView
*
* Author: Jack-Chen
*
* Time 16/5/27 下午3:45
*/
public class LinearLayoutForListView extends LinearLayout {
private BaseAdapter adapter;
private OnClickListener onClickListener = null;
/**
* 绑定布局
*/
public void bindLinearLayout() {
int count = adapter.getCount();
this.removeAllViews();
for (int i = 0; i < count; i++) {
View v = adapter.getView(i, null, null);
v.setOnClickListener(this.onClickListener);
addView(v, i);
}
Log.v("countTAG", "" + count);
}
public LinearLayoutForListView(Context context) {
super(context);
}
2.2.2: 将自己之前的ListView布局文件替换为这个包下的布局文件
2.2.3 : 然后去替换Activity或Fragment中之前ListView的控件为LinearLayoutForListView,最后为其setAdapter适配数据即可
方式三:自定义MyListView直接继承系统的ListView,重写onMeasure()方法
/**
* Describe: 自定义MyListView直接继承系统的ListView
*
* Author: Jack-Chen
*
* Time 16/8/27 下午2:40
*/
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 解决ScrollView嵌套ListView显示不全问题
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//heightMeasureSpec 参数1是32位的值 右移2位变成30位的值, MeasureSpec.AT_MOST是模式,ListView源码中应该要执行MeasureSpec.AT_MOST这个if
// if (heightMode == MeasureSpec.AT_MOST) {
// // TODO: after first layout we should maybe start at the first visible position, not 0
// heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
// }
//而不能让执行这个if,因为这个if里边刚好是listview的1个item的高度
// if (heightMode == MeasureSpec.UNSPECIFIED) {
// heightSize = mListPadding.top + mListPadding.bottom + childHeight +
// getVerticalFadingEdgeLength() * 2;
// }
heightMeasureSpec =
MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2 , MeasureSpec.AT_MOST) ;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
这个方法我相信绝大部分的人都是采用这用方式的,因为这种方式相对来说比较简单,直接拷贝过去用即可,但是为什么要这样去写,重写onMeasure()方法后,里边的参数为什么是右移2位,然后模式给他设置为MeasureSpec.AT_MOST呢,接下来我给大家来分析下,为什么这样去写,大神可以跳过哈
继承ListView后,大家可以直接点击super.onMeasure(widthMeasureSpec, heightMeasureSpec);进入ListView的源码,可以看到
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽高的模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取前两位
final int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取前两位
//获取宽高的值
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取后面30位
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = 0;
int childHeight = 0;
int childState = 0;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
measureScrapChild(child, 0, widthMeasureSpec, heightSize);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState & MEASURED_STATE_MASK);
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
在这里要给大家说一下,widthMeasureSpec和heightMeasureSpec分别都包含了2个信息
Integer.MAX_VALUE是一个32位的值,右移两位会将Integer.MAX_VALUE变为一个30位的值,最后两位就是MeasureSpec.AT_MOST
那么由这个可知:
final int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取前两位
final int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取前两位
//获取宽高的值
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取后面30位
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
heightMode 就是MeasureSpec.AT_MOST
heightSize 就是Integer.MAX_VALUE>>2
因为高度heightMode传递的是MeasureSpec.AT_MOST,所以就只会进到if (heightMode == MeasureSpec.AT_MOST)中,而不会进到heightMode == MeasureSpec.UNSPECIFIED中
为什么使用Integer.MAX_VALUE>>2:
这个Integer.MAX_VALUE是表示32位的一个值,然后右移两位,表示有30为的值,表示大小,后边的MeasureSpec.AT_MOST是表示model,模式
为什么使用MeasureSpec.AT_MOST:
大家可以看到源码是重写onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,其他的我们可以不去看他,直接看里边有2个if判断高度的,宽度的不需要看,其中第一个if是判断高度的模式 heightMode == MeasureSpec.UNSPECIFIED,里边代码表示距离上边+距离下边+子高度刚好表示一个item的高度,那么现在我们再来回过头想下,之前ScrollView嵌套ListView只显示一条,我们猜想它应该是走的这个if判断里边,我们要做的就是不要让它执行这个if,而是要让它执行下边的if (heightMode == MeasureSpec.AT_MOST)判断,而这里的判断可以直接点击进去heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);看下
而且它传递的也是上边获取的高度heightSize,到这里我们就知道为什么第二个参数是
MeasureSpec.AT_MOST,如果我们不重写onMeasure()方法,其实它里边的高度heightMeasureSpec默认是执行heightMode == MeasureSpec.UNSPECIFIED,所以高度才会显示不全
如果你觉得有帮助,可以关注我,我会持续更新博客,会将自己项目中遇到的问题、遇到的bug、以及解决方法都会分享出来,也许可能像我这样的文章或者解决方法网上一搜都有,不过也没有关系,自己觉得还是写出来会比较踏实,因为这些都是自己用过的、思考过的一些东西,还是觉得蛮有用的