上一篇说明了事件分发的机制,接下来以一个实际会遇到的场景继续学习事件分发机制,
场景2:ScrollView嵌套ListView,listview只能显示一个item。
分析:
既然是高度问题的话那就先打开ScrollView查看一下onMeasure()方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//scrollview继承自FrameLayout,所以执行了framelayout的onMeasure()方法
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//mFillViewport默认为false
if (!mFillViewport) {
return;
}
...
一步一步往下看,
FrameLayout的onMeasure():
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//最重要的是这一步,测量子view,且该方法scrollview已重写
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
重要的就是measureChildWithMargins()这个方法,且srcollview已重写过,所以再看该方法:
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//listview的宽模式
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
//listview的长模式,(留意,这里强行的吧listview的模式设置UNSPECIFIED),这里就是让listview只显示一个item的最主要原因。
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
//测量child。
//childWidthMeasureSpec、childWidthMeasureSpec就是listview的测量模式
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
到这里我们知道,scrollview会强行设置他的childview的高模式为UNSPECIFIED,那接下来再看一下listview设置这种模式的话是如何计算其高度的:
listview-onMeasure():
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
意思就是,在该测量模式下,listview的高度等于垂直方向上的padding+第一个子child的高度+??不影响判断。
至此scrollview嵌套listivew结果只显示一个item的原因已经分析清楚了。
小结:scrollview默认把childview设置为UNSPEFEIED模式,而该模式下的listview给自己的测量的高度就是第一个item的高度。
解决方案:
1.设置mFillViewport为true
看完整的ScrollView的onMeasure()代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//如果mFillViewport为true的话,则后面的代码都能执行,那么会有怎样的效果呢?
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (targetSdkVersion >= VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}
final int desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
//测量模式为EXACTLY
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
//留意childHeightMeasureSpec的测量模式
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
如果能执行到后面的代码,那么listview就能全部显示。也就是说,mFillViewport的值要设置为true就能解决问题。
可以直接在scrollview的布局中加属性fillViewport = true;也可以动态设置。
2.自定义ListView:
分析源码的目的就是要清楚产生问题的原因,从而能够针对性的提出解决办法,listview只显示一个也是因为自己在onMeasure()测量的问题造成的,毕竟是listview返回一个item的高度给scrollview,scrollview才显示一个item的高度的。所以我们也可以自定义Listview,并重写onMeasure()方法,返回正常的高度给ScrollView。
自定义的ListView只需重写onMeasure()方法即可:
@Override
//>>2右移两位是因为MeasureSpec前2是表示模式,后30位才是尺寸
int customHeightSpec =
MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, customHeightSpec);
}
还有一种思路就是在用LinearLayout包含listview,那么LinearLayout被设置为UNSPECFIED模式,此时listview必须设置精确的dp值。
如下图,当linearLayout为UNSPECIFIED时,listview必须是精确dp时才能避免也被设置为UNSPECIFIED模式。
这里并未涉及到更多的事件分发的机制。
下一篇我将分析ListView嵌套Scrollview产生的问题,来更好的理解事件分发机制。
感谢阅读!