美文网首页代码Android开发Android开发
006 采坑-NestedScrollView嵌套ViewPag

006 采坑-NestedScrollView嵌套ViewPag

作者: 凤邪摩羯 | 来源:发表于2021-09-13 09:13 被阅读0次

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

    相关文章

      网友评论

        本文标题:006 采坑-NestedScrollView嵌套ViewPag

        本文链接:https://www.haomeiwen.com/subject/cobmaktx.html