我们都知道 ScrollView 嵌套 ListView 会导致高度塌陷,也都知道各种各样的解决办法,其中最常见的一种方法就是重写 ListView 的 onMeasure() 方法,然后进行一些操作,最后就可以解决这个问题。
问题是解决了,但是为什么呢?带着这个问题,开始我们今天的探索之旅!
AT_MOST 是个什么鬼
public class ListViewForScrollView extends ListView {
public ListViewForScrollView(Context context) {
super(context);
}
public ListViewForScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListViewForScrollView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
@Override
/**
* 重写该方法,达到使ListView适应ScrollView的效果
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 打包方法,重新构造 heightMeasureSpec
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
我们知道,Android 的测绘机制是一个递归的流程,从最顶层的开始,依次递归向下测量子 View ,即调用 measureChild() 方法,一层层测量后,最后再测量最外层的 ViewGroup .
查看 ScrollView 的源码:
@Override
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
+ mPaddingRight, lp.width);
final int verticalPadding = mPaddingTop + mPaddingBottom;
// ScrollView 在具体测量子 View 时,向下传递的测量规格为 MeasureSpec.UNSPECIFIED
childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
理论上讲,这时候代码会走到 ListView 的 onMeasure() 方法中:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childHeight = 0;
... ...
// 获取 ListView 的高度,此时应该只有一个条目的高度
childHeight = child.getMeasuredHeight();
... ...
// 重点就在这里
// 如果测量模式为 MeasureSpec.UNSPECIFIED,则最终的高度就是已测量的高度 + padding
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
// 如果为 AT_MOST ,则会调用 measureHeightOfChildren() 方法,重新计算 View 高度
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
}
Integer.MAX_VALUE >> 2 又是个啥
故事讲到这里,还没有结束。
我们已经知道了在解决嵌套问题的时候为什么要使用 AT_MOST,但是好像还不理解第一个参数 Integer.MAX_VALUE >> 2 到底是个什么鬼?接下来我们来解决这个疑惑。
首先,理论基础:
MeasureSpec 是一个 32 位的 int 值,前 2 位表示 SpecMode,后 30 位表示 SpecSize。
MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,同样在使用到具体的属性时,可以通过解包的方式来获取原始值。
很明显,SpecMode 已经知道了,就是 AT_MOST;那么,由于 SpecSize 是一个 30 位的值,因此需要对此处传入的参数进行一个右移两位的操作,也就是这个 >>2 的动作;至于 Integer.MAX_VALUE , 则是希望这个值尽可能的大,从而不对 item 的高度造成约束。
具体查看源码:
// 这里是接着上面的代码,测量规格设置为 AT_MOST 以后,进入的测量子 View 高度的方法
// 注意此处的 maxHeight,就是我们传入的 Integer.MAX_VALUE,顾名思义,最大高度
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
... ...
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap);
... ...
// 继续追踪代码,进入这个方法;同样将子 View,最大高度等参数传入
measureScrapChild(child, i, widthMeasureSpec, maxHeight);
... ...
}
// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight
return returnedHeight;
}
private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
// 获取子 View 的 LayoutParams, 如果没有,就初始化一个
// new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
// ViewGroup.LayoutParams.WRAP_CONTENT, 0);
LayoutParams p = (LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
child.setLayoutParams(p);
}
// 此处获取 LayoutParams 的高度值
// 我们知道,MATCH_PARENT 的值为 -1,WRAP_CONTENT 的值为 -2
// 所以,除非是开发者在布局文件或代码中指明了高度的确切值,否则 lpHeight > 0 就不会成立
final int lpHeight = p.height;
final int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
// 代码会走到这里
// 将传下来的 Integer.MAX_VALUE >> 2 与 MeasureSpec.UNSPECIFIED 一起
// 打包成当前子 View 的 测量规格
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
}
// 递归向下,调用子 View 的 measure() 方法,而在 measure() 方法中又会去实际调用当前 View 的
// onMeasure() 方法来进行测量
child.measure(childWidthSpec, childHeightSpec);
// Since this view was measured directly aginst the parent measure
// spec, we must measure it again before reuse.
child.forceLayout();
}
这里我们以 TextView 为例:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
// if 判断不成立,会进入 else 分支
// 调用 getDesiredHeight() 方法得到当前 View 的高度
// 在其中会将测量得到的,控件所需要的最小高度,与我们传入的高度做对比,然后取较小值
// 这也就是为什么我们传入的值虽然很大,但是最终不会对控件高度造成影响
// 相反,如果传入的值过小,就有可能因为这个值的限制,导致最终显示不全
int desired = getDesiredHeight();
height = desired;
mDesiredHeightAtMeasure = desired;
// 条件不满足
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
}
int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
}
// 设置最终的宽高信息
setMeasuredDimension(width, height);
}
终于写完了,收工。