我们项目中经常可能会用到类似的以下布局。
在这种情况下,RecyclerView会经常无法测量出来实际的高度,我一开始以为RecyclerView类似于ListView对MeasureSpec.UNSPECIFIED做了直接高度处理而无法正确测量,但我查看源码后发现不是这样的。网上查找的解决方法是嵌套RelativeLayout。那么为什么RelativeLayout能正确测量而LinearLayout却不行呢,直接查看源码来简单了解一下。
首先我们查看ScrollView的onMeasure方法。onMeasure调用了super,然后走到measureChildWithMargins()
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这里我们只关注这里
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
childHeightMeasureSpec 也就是mode是UNSPECIFIED,size是ScrollView的高度,这里usedTotal=0。
说实话LinearLayout的OnMeasure真的相当的长,但是我们也只需要找关键的地方,weight的处理可以暂时不用管。
在measureVertical()方法中我们直接找到测量规格是如何传入子View的。
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);
其实我可以按照实现的情况来猜测,其实就是将LinearLayout的测量规格加上child已经使用的控件来综合判断下一个child是什么规格的。我接着看源码。
直接调用了ViewGroup的measureChildWithMargins
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
那么根据实际情况就可以再猜测这个方法getChildMeasureSpec(),应该是使用LinearLayout的测量规格的高度减去已经使用的高度,以及childe的padding,margin来生成。
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 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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这里就很明确了,mode是UNSPECIFIED,至于高度就是计算后剩余的高度,是有一个具体值的。
RecyclerView的OnMeasure也是真的看的人头疼的一匹,好在这个可以断点调试,话不多说,也直接看关键代码吧。
我们就直接看LinearLayoutManager的onLayoutChildren()方法。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
if (mAnchorInfo.mLayoutFromEnd) {
fill(recycler, mLayoutState, state, false);
...
} else {
...
}
...
省略了很多代码,接着到fill方法中RecyclerView就会添加View了。
接着在fill中我们可以看到
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
...
return start - layoutState.mAvailable;
}
这个while循环就是给RecyclerView添加控件,如果高度不足或者就会停止添加。上面我们说了LinearLayout会传一个固定高度过来,这个remainingSpace 会不断的减小,导致无法填充所有控件。而layoutState.mInfinite这个变量是在哪里置位的呢?回到上一步,resolveIsInfinite()这个方法中,可以找到。
boolean resolveIsInfinite() {
return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
&& mOrientationHelper.getEnd() == 0;
}
这个mOrientationHelper的实例就是
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
...
@Override
public int getEnd() {
return mLayoutManager.getHeight();
}
...
@Override
public int getMode() {
return mLayoutManager.getHeightMode();
}
...
};
}
绕了一圈又回来了,找到LinearLayoutManager的设置规格方法
void setMeasureSpecs(int wSpec, int hSpec) {
mWidth = MeasureSpec.getSize(wSpec);
mWidthMode = MeasureSpec.getMode(wSpec);
if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
mWidth = 0;
}
mHeight = MeasureSpec.getSize(hSpec);
mHeightMode = MeasureSpec.getMode(hSpec);
if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
mHeight = 0;
}
}
ALLOW_SIZE_IN_UNSPECIFIED_SPEC这个变量是判断api版本是否大于23,目前版本是23以上的。那么mHeight就不是0,是一个确定的值,综合前面layoutState.mInfinite就是false了,在剩余空间不够的情况下,RecyclerView不会继续填充控件,导致LinearLayout包裹的时候无法准确测量高度。换一个低版本的模拟器测试,不出意外在API21的模拟器上可以准确的测量出来高度。那么接下来就分析RelativeLayout的测量了。
RelativeLayout的onMeasure要测量两次,我们分开来看。
首先
private void measureChildHorizontal(
View child, LayoutParams params, int myWidth, int myHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
myWidth);
final int childHeightMeasureSpec;
if (myHeight < 0 && !mAllowBrokenMeasureSpecs) {
if (params.height >= 0) {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
params.height, MeasureSpec.EXACTLY);
} else {
// Negative values in a mySize/myWidth/myWidth value in
// RelativeLayout measurement is code for, "we got an
// unspecified mode in the RelativeLayout's measure spec."
// Carry it forward.
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
} else {
final int maxHeight;
if (mMeasureVerticalWithPaddingMargin) {
maxHeight = Math.max(0, myHeight - mPaddingTop - mPaddingBottom
- params.topMargin - params.bottomMargin);
} else {
maxHeight = Math.max(0, myHeight);
}
final int heightMode;
if (params.height == LayoutParams.MATCH_PARENT) {
heightMode = MeasureSpec.EXACTLY;
} else {
heightMode = MeasureSpec.AT_MOST;
}
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这里myHeight 是等于-1,小于0,mAllowBrokenMeasureSpecs 在api<17的时候为true,所以进入第一个判断。
RecyclerView的LayoutPara的height是wrap_content,也就是-2,所以就直接执行了
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
看到这里结合上面的就明白了,layoutState.mInfinite就为true了。
第二次测量
private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {
int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft,
params.mRight, params.width,
params.leftMargin, params.rightMargin,
mPaddingLeft, mPaddingRight,
myWidth);
int childHeightMeasureSpec = getChildMeasureSpec(params.mTop,
params.mBottom, params.height,
params.topMargin, params.bottomMargin,
mPaddingTop, mPaddingBottom,
myHeight);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这里我们的RecyclerView没有任何above,below之类的规则,params.mTop,
params.mBottom,都会被设置成一个VALUE_NOT_SET的值。然后我再进入方法
private int getChildMeasureSpec(int childStart, int childEnd,
int childSize, int startMargin, int endMargin, int startPadding,
int endPadding, int mySize) {
int childSpecMode = 0;
int childSpecSize = 0;
final boolean isUnspecified = mySize < 0;
if (isUnspecified && !mAllowBrokenMeasureSpecs) {
if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
// Constraints fixed both edges, so child has an exact size.
childSpecSize = Math.max(0, childEnd - childStart);
childSpecMode = MeasureSpec.EXACTLY;
} else if (childSize >= 0) {
// The child specified an exact size.
childSpecSize = childSize;
childSpecMode = MeasureSpec.EXACTLY;
} else {
// Allow the child to be whatever size it wants.
childSpecSize = 0;
childSpecMode = MeasureSpec.UNSPECIFIED;
}
return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
}
...
}
这里的mysize<0,childSize=-2,很明显又生成了一个相同的规格
childSpecSize = 0;
childSpecMode = MeasureSpec.UNSPECIFIED;
看到这里我们就明白了,为什么LinearLayout不行,而RelativeLayout则可以成功测量出高度了。根据原理稍微对RecyclerView改造一下
/*
* Created by TY on 2018/4/10.
*/
public class NestRecyclerView extends RecyclerView {
public NestRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setLayoutManager(new LinearLayoutManager(context));
setNestedScrollingEnabled(false);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int newHeightSpec=MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);
super.onMeasure(widthSpec, newHeightSpec);
}
}
我直接将LinearLayoutManager设置进去了,加上设置了嵌套滑动为false,将不会有滑动的冲突了,这个RecyclerView测试可以直接测量wrap_content的高度了。最后再说的一点是,我用的是27.0.2的版本的,其他版本可能有变换而效果不同,这里我就不多做测试了。