ScrollView嵌套RecyclerView解决以及原理详解

我们项目中经常可能会用到类似的以下布局。
这里写图片描述
在这种情况下,RecyclerView会经常无法测量出来实际的高度,我一开始以为RecyclerView类似于ListView对MeasureSpec.UNSPECIFIED做了直接高度处理而无法正确测量,但我查看源码后发现不是这样的。网上查找的解决方法是嵌套RelativeLayout。那么为什么RelativeLayout能正确测量而LinearLayout却不行呢,直接查看源码来简单了解一下。

一、ScrollView的onMeasure

首先我们查看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

说实话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

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

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的版本的,其他版本可能有变换而效果不同,这里我就不多做测试了。

猜你喜欢

转载自blog.csdn.net/a568478312/article/details/79881540