NestedScrollView嵌套RecyclerView导致RecyclerView复用失效的原因?

一、问题描述

使用NestedScrollView嵌套RecyclerView导致RecyclerView复用失效,RecyclerView会将所有数据一次性全部加载。
布局文件如下:



    

Adapter的onBindViewHolder打印日志代码如下:

    public void onBindViewHolder(final ViewHolder holder, final int position) {
        Log.d("tag", "onBindViewHolder>>" + holder.itemView.toString());
    }

日志如下

onBindViewHolder>>android.widget.LinearLayout{5853c0e
onBindViewHolder>>android.widget.LinearLayout{a7e243c
onBindViewHolder>>android.widget.LinearLayout{95d0b1a
onBindViewHolder>>android.widget.LinearLayout{e274828
onBindViewHolder>>android.widget.LinearLayout{cb0bee6
onBindViewHolder>>android.widget.LinearLayout{4bcbed4
onBindViewHolder>>android.widget.LinearLayout{a39e372
onBindViewHolder>>android.widget.LinearLayout{a90f440
onBindViewHolder>>android.widget.LinearLayout{cbec4be
onBindViewHolder>>android.widget.LinearLayout{cb1146c
onBindViewHolder>>android.widget.LinearLayout{31e6eca
onBindViewHolder>>android.widget.LinearLayout{9d10b58
onBindViewHolder>>android.widget.LinearLayout{91cad96
onBindViewHolder>>android.widget.LinearLayout{5f78504
onBindViewHolder>>android.widget.LinearLayout{ee0d22 
onBindViewHolder>>android.widget.LinearLayout{ce9ed70
onBindViewHolder>>android.widget.LinearLayout{983d96e
onBindViewHolder>>android.widget.LinearLayout{f58709c
onBindViewHolder>>android.widget.LinearLayout{d981e7a
onBindViewHolder>>android.widget.LinearLayout{6c9fa88

我们在Adapter中加载20条数据,RecylerView就所有数据一次性显示完了。这样在数据量小不会有问题,数据量大时就会造成卡顿或者OOM。

二、原因分析

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
     }

NestedScrollView的onMeasure方法如上。会调用父类的onMeasure。NestedScrollView是继承FrameLayout,因此会调用FrameLayout的onMeasure。FrameLayout的onMeasure代码如下:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
             }
         }    
     }    

FrameLayout的onMeasure会循环调用measureChildWithMargins测量子View。
因为NestedScrollView重写了measureChildWithMargins,因此我们应该看NestedScrollView的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,
                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这里将高度的测量模式指定为 MeasureSpec.UNSPECIFIED。一般情况topMargin和bottomMargin指定为0,因此高度的测量值是0。
RecyclerView的measure和layout最终都交给LayoutManager完成。上面例子使用的LayoutManager是LinearLayoutManager,RecyclerView的measure和layout最终会执行到LinearLayoutManager的fill方法。

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
            ...
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
        return start - layoutState.mAvailable;
    }

fill方法在while循环中设置子item的布局。layoutState.hasMore(state)是当还有item就为true,layoutState.mInfinite的赋值如下:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
	...
	mLayoutState.mInfinite = resolveIsInfinite();
	...
	fill(recycler, mLayoutState, state, false);
	...
}

resolveIsInfinite源码如下:

    boolean resolveIsInfinite() {
        return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
                && mOrientationHelper.getEnd() == 0;
    }

getMode和getEnd的源码如下:

    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();
            }
			...
        };
    }

实际调用mLayoutManager的方法。

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
	public abstract static class LayoutManager {
		...
	    @Px
        public int getHeight() {
            return mHeight;
        }
        
        public int getHeightMode() {
            return mHeightMode;
        }
        ...
    }
}

在RecyclerView的onMeasure会调用setMeasureSpecs:

protected void onMeasure(int widthSpec, int heightSpec) {
	...
	mLayout.setMeasureSpecs(widthSpec, heightSpec);
	...
}

setMeasureSpecs源码如下:

        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;
            }
        }

在这里会初始化mHeight和mHeightMode,即使用父控件NestedScrollView传入的测量高度进行赋值,因此上面例子得到的mHeight =0,mHeightMode =MeasureSpec.UNSPECIFIED。所以上面的mLayoutState.mInfinite会返回true,在fill中加载子item时就会一直加载所有的item。

三、如何解决

要解决上面问题,只需要NestedScrollView测量RecyclerView不使用MeasureSpec.UNSPECIFIED模式即可。因此可以重写measureChildWithMargins。

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        child.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);
    }

你可能感兴趣的:(NestedScrollView嵌套RecyclerView导致RecyclerView复用失效的原因?)