背景:
这两天在做一个画面,如图所示
于是我用了NestedScrollView嵌套ViewPager嵌套RecyclerView显示。
现象:
下面的RecyclerView只能显示屏幕所在的两行,并且无法上下滑动。
原因:
ps:找寻问题的源头的过程比较艰辛,这里就不赘述了,直接上源码解释
- RecyclerView的测量:
RecyclerView的测量过程比较复杂。可以参考这篇文章RecyclerView绘制原理探究
从这篇文章得知,RecyclerView是在LinearLayoutManager的layoutChunk方法里面开始填充子View的。那么我们来看下fill方法是怎么实现的:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
...代码省略...
}
next方法里面会执行Adapter里面的onCreateViewHolder和onBindViewHolder方法,这里就不再展开了。从layoutChunk代码可以看出只是add了一个View,那么为什么RecyclerView能展示这么多条数据呢,可想而知调用layoutChunk的地方加了while循环了。那我们就退回去看调用layoutChunk的地方-——fill方法:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...代码省略..
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
layoutState.hasMore如果为false那么就代表整个列表数据已经绘制完成了。remainSpace代表RecyclerView填充屏幕时所剩下的高度。layoutState.mInfinite从名字大概可以猜测是否要无限测量绘制的flag。
那么从bug现象可以看出来这里只绘制了两条数据,所以layoutState.hasMore肯定为true。要想跳出循环,必须remainSpace<=0且layoutState.mInfinite为false。
当绘制到屏幕底部是,remainSpace自然就<=0了。剩下就来看看为什么mInfinite是false了。那我们可以看看mInfinite是什么时候赋值的:
/**
* {@inheritDoc}
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...代码省略...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
if (mAnchorInfo.mLayoutFromEnd) {//此处由于没有做特殊设置,RecyclerView默认测量都是从上往下测量的因此true的条件代码省略
...代码省略...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
...代码省略...
}
boolean resolveIsInfinite() {
return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
&& mOrientationHelper.getEnd() == 0;
}
看判断条件可以得知,当RecyclerView的SpecMode是UNSPECIFIED,并且此时RecyclerView的SpecSize是0的时候设置为true,其他情况都为false,因为此方法调用在fill前面,所以此时RecyclerView的高度必然为0,那么问题就出在SpecMode里面了。
- 上一层ViewGroup是ViewPager,那再来看下ViewPager的测量源码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...代码省略..
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);//罪魁祸首
// Make sure we have created all fragments that we need to have shown.
mInLayout = true;
populate();
mInLayout = false;
// Page views next.
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
if (DEBUG) {
Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) {
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
好家伙,ViewPager将子View的mode直接设置为EXACTLY,终于找到了罪魁祸首了,虽然不知道为啥ViewPager要这么设置,但是这么放着不管肯定不行。那么我们就开始改造ViewPager吧
思路就是仿照这段代码直接在自定义Viewpager里面测量,然后将child的heightSpecMode设置为MeasureSpec.UNSPECIFIED问题就能解决了:
int height = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if(child.getVisibility() != GONE){
child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int h = child.getMeasuredHeight();
if(h > height){
height = h;
}
}
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
这个问题解决方案虽然很简单,但是中间涉及到了ViewGroup的嵌套调用,找寻问题源头耗费了一天的时间。不过总的来说解决了还是比较欣慰的