首先在scrollview中嵌入listview,我们需要动态设置listview的高度,否则只会展示一行。
public void fixListViewHeight(ListView listView) { // 如果没有设置数据适配器,则ListView没有子项,返回。 ListAdapter listAdapter = listView.getAdapter(); int totalHeight = 0; if (listAdapter == null) { return; } for (int i = 0, len = listAdapter.getCount(); i < len; i++) { View listViewItem = listAdapter.getView(i , null, listView); // 计算子项View 的宽高 listViewItem.measure(0, 0); // 计算所有子项的高度和 totalHeight += listViewItem.getMeasuredHeight(); } ViewGroup.LayoutParams params = listView.getLayoutParams(); // listView.getDividerHeight()获取子项间分隔符的高度 // params.height设置ListView完全显示需要的高度 params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); listView.setLayoutParams(params); }但是当item高度不一样的时候,或者可能一个item的高度就占满一个屏幕,或者一个屏幕还不够展示一个较宽的item的时候,那么就会出现item展示不全问题。
我看网上有人说讲述的方法进行修正:
public void fixListViewHeight(ListView listView) { // 如果没有设置数据适配器,则ListView没有子项,返回。 ListAdapter listAdapter = listView.getAdapter(); int totalHeight = 0; if (listAdapter == null) { return; } for (int i = 0, len = listAdapter.getCount(); i < len; i++) { View listViewItem = listAdapter.getView(i , null, listView); //根据屏幕宽度计算高度 int width = View.MeasureSpec.makeMeasureSpec( _mActivity.getWindowManager().getDefaultDisplay().getWidth(), View.MeasureSpec.EXACTLY); //依据宽度计算高度 int height = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED); // 计算子项View 的宽高 listViewItem.measure(0, 0); // 计算所有子项的高度和 totalHeight += listViewItem.getMeasuredHeight(); } ViewGroup.LayoutParams params = listView.getLayoutParams(); // listView.getDividerHeight()获取子项间分隔符的高度 // params.height设置ListView完全显示需要的高度 params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); listView.setLayoutParams(params); }但是这个方法还是不对的。
出现上述问题,主要原因是我们的item里面只能使用LinearLayout,因为使用其他父布局(如relativelayout)没有measure方法,会爆出异常。但是LinearLayout方法默认的measure方法使用的是MeasureSpec.EXACTLY 计算方法。
计算子view中高度,是由父view通知计算的: -------- 下面是剖析原理,不想看原理可以直接看解决方法,但是不一定可以解决你的问题,因为和布局有关系(原理中介绍)
那么父容器怎么把这些要求告诉子View呢?MeasureSpec其实就是承担这种作用:MeasureSpec是父控件提供给子View的一个参数,作为设定自身大小参考,只是个参考,要多大,还是View自己说了算。先看下MeasureSpec的构成,MeasureSpec由size和mode组成,mode包括三种,UNSPECIFIED、EXACTLY、AT_MOST,size就是配合mode给出的参考尺寸,具体意义如下:
-
UNSPECIFIED(未指定),父控件对子控件不加任何束缚,子元素可以得到任意想要的大小,这种MeasureSpec一般是由父控件自身的特性决定的。比如ScrollView,它的子View可以随意设置大小,无论多高,都能滚动显示,这个时候,size一般就没什么意义。
-
EXACTLY(完全),父控件为子View指定确切大小,希望子View完全按照自己给定尺寸来处理,跟上面的场景1跟2比较相似,这时的MeasureSpec一般是父控件根据自身的MeasureSpec跟子View的布局参数来确定的。一般这种情况下size>0,有个确定值。
-
AT_MOST(至多),父控件为子元素指定最大参考尺寸,希望子View的尺寸不要超过这个尺寸,跟上面场景3比较相似。这种模式也是父控件根据自身的MeasureSpec跟子View的布局参数来确定的,一般是子View的布局参数采用wrap_content的时候。
先来看一下ViewGroup源码中measureChild怎么为子View构造MeasureSpec的:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
由于任何View都是支持Padding参数的,在为子View设置参考尺寸的时候,需要先把自己的Padding给去除,这同时也是为了Layout做铺垫。接着看如何getChildMeasureSpec获取传递给子View的MeasureSpec的:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
当子View接收到父控件传递的MeasureSpec的时候,就可以知道父控件希望自己如何显示,这个点对于开发者而言就是onMeasure函数,先来看下View.java中onMeasure函数的实现:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
其中getSuggestedMinimumWidth是根据设置的背景跟最小尺寸得到一个备用的参考尺寸,接着看getDefaultSize,如下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以看到,如果自定义View没有重写onMeasure函数,MeasureSpec.AT_MOST跟MeasureSpec.AT_MOST的表现是一样的,也就是对于场景2跟3的表现其实是一样的,也就是wrap_content就跟match_parent一个效果,现在我们知道MeasureSpec的主要作用:父控件传递给子View的参考,那么子View拿到后该如何用呢?接收到父控件传递的MeasureSpec后,View应该如何用来处理自己的尺寸呢?onMeasure是View测量尺寸最合理的时机,如果View不是ViewGroup相对就比较简单,只需要参照MeasureSpec,并跟自身需求来设定尺寸即可,默认onMeasure的就是完全按照父控件传递MeasureSpec设定自己的尺寸的。这里重点讲一下ViewGroup,为了获得合理的宽高尺寸,ViewGroup在计算自己尺寸的时候,必须预先知道所有子View的尺寸,举个例子,用一个常用的流式布局FlowLayout来讲解一下如何合理的设定自己的尺寸。
先分析一下FLowLayout流式布局(从左到右)的特点:FLowLayout将所有子View从左往右依次放置,如果当前行,放不开的就换行。从流失布局的特点来看,在确定FLowLayout尺寸的时候,我们需要知道下列信息:
-
父容器传递给FlowLayout的MeasureSpec推荐的大小(超出了,显示不出来,又没意义)
-
FlowLayout中所有子View的宽度与宽度:计算宽度跟高度的时候需要用的到。
-
综合MeasureSpec跟自身需求,得出合理的尺寸
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
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:
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);
}
可以看到:
-
如果父控件传递给的MeasureSpec的mode是MeasureSpec.UNSPECIFIED,就说明,父控件对自己没有任何限制,那么尺寸就选择自己需要的尺寸size
-
如果父控件传递给的MeasureSpec的mode是MeasureSpec.EXACTLY,就说明父控件有明确的要求,希望自己能用measureSpec中的尺寸,这时就推荐使用MeasureSpec.getSize(measureSpec)
-
如果父控件传递给的MeasureSpec的mode是MeasureSpec.AT_MOST,就说明父控件希望自己不要超出MeasureSpec.getSize(measureSpec),如果超出了,就选择MeasureSpec.getSize(measureSpec),否则用自己想要的尺寸就行了
对于FlowLayout,可以假设每个子View都可以充满FlowLayout,因此,可以直接用measureChildren测量所有的子View的尺寸:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
int paddingTop = getPaddingTop();
int count = getChildCount();
int maxWidth = 0;
int totalHeight = 0;
int lineWidth = 0;
int lineHeight = 0;
int extraWidth = widthSize - paddingLeft - paddingRight;
<!--直接用measureChildren测量所有的子View的高度-->
measureChildren(widthMeasureSpec, heightMeasureSpec);
<!--现在可以获得所有子View的尺寸-->
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
if (view != null && view.getVisibility() != GONE) {
if (lineWidth + view.getMeasuredWidth() > extraWidth) {
totalHeight += lineHeight ;
lineWidth = view.getMeasuredWidth();
lineHeight = view.getMeasuredHeight();
maxWidth = widthSize;
} else {
lineWidth += view.getMeasuredWidth();
}
<!--获取每行的最高View尺寸-->
lineHeight = Math.max(lineHeight, view.getMeasuredHeight());
}
}
totalHeight = Math.max(totalHeight + lineHeight, lineHeight);
maxWidth = Math.max(lineWidth, maxWidth);
<!--totalHeight 跟 maxWidth都是FlowLayout渴望得到的尺寸-->
<!--至于合不合适,通过resolveSize再来判断一遍,当然,如果你非要按照自己的尺寸来,也可以设定,但是不太合理-->
totalHeight = resolveSize(totalHeight + paddingBottom + paddingTop, heightMeasureSpec);
lineWidth = resolveSize(maxWidth + paddingLeft + paddingRight, widthMeasureSpec);
setMeasuredDimension(lineWidth, totalHeight);
}
可以看到,设定自定义ViewGroup的尺寸其实只需要三部:
-
测量所有子View,获取所有子View的尺寸
-
根据自身特点计算所需要的尺寸
-
综合考量需要的尺寸跟父控件传递的MeasureSpec,得出一个合理的尺寸
传递给子View的MeasureSpec是父容器根据自己的MeasureSpec及子View的布局参数所确定的,那么根MeasureSpec是谁创建的呢?我们用最常用的两种Window来解释一下,Activity与Dialog,DecorView是Activity的根布局,传递给DecorView的MeasureSpec是系统根据Activity或者Dialog的Theme来确定的,也就是说,最初的MeasureSpec是直接根据Window的属性构建的,一般对于Activity来说,根MeasureSpec是EXACTLY+屏幕尺寸,对于Dialog来说,如果不做特殊设定会采用AT_MOST+屏幕尺寸。这里牵扯到WindowManagerService跟ActivityManagerService,感兴趣的可以跟踪一下WindowManager.LayoutParams 。
-------------------------------- 原理剖析完毕
解决方案:
上述问题解决方案,重写listview,去重写measure方法即可:
public class CommListView extends ListView { public CommListView(Context context) { super(context); // TODO Auto-generated constructor stub } public CommListView(Context context, AttributeSet attrs) { super(context, attrs); } public CommListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if(ev.getAction() == MotionEvent.ACTION_MOVE){ return true; } return super.dispatchTouchEvent(ev); } }