踩坑之路:NestedScrollView嵌套ViewPager嵌套RecyclerView

背景:

这两天在做一个画面,如图所示



于是我用了NestedScrollView嵌套ViewPager嵌套RecyclerView显示。

现象:

下面的RecyclerView只能显示屏幕所在的两行,并且无法上下滑动。

原因:

ps:找寻问题的源头的过程比较艰辛,这里就不赘述了,直接上源码解释

  1. 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里面了。

  1. 上一层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的嵌套调用,找寻问题源头耗费了一天的时间。不过总的来说解决了还是比较欣慰的

你可能感兴趣的:(踩坑之路:NestedScrollView嵌套ViewPager嵌套RecyclerView)