前言:通过NestedScrollView嵌套RecyclerView可以轻松实现嵌套滑动,但我们会发现RecyclerView懒加载失效了。

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:descendantFocusability="blocksDescendants">
        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="@color/colorAccent"
            android:gravity="center"
            android:text="这是头部" />
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            android:orientation="vertical"/>
    </LinearLayout>
</androidx.core.widget.NestedScrollView>

原因:NestedScrollView高度虽然为屏幕高度,但其对子布局会进行wrap_content方式测量,这里LinearLayout即使是match_parent也不是屏幕高度,因此传递给其子RecyclerView也wrap_content方式得到的高度,导致RecyclerView一次加载出全部数据。(NestedScrollView嵌套滑动原理是还是平面式的滑动,RecyclerView由于加载了全部数据,本身不再滑动而是随着将LinearLayout移动实现的)

方案:要使RecyclerView懒加载则不能使其高度包裹所有item,需要指定最大高度(本例中最大高度为屏幕高度,先是NestedScrollView响应滑动当LinearLayout至最底部时,此时RecyclerView正好显示全,接下来再滑动就由RecyclerView来完成,这才是真正的嵌套滑动)

1、自定义RecyclerView使其支持设置最大高度

public class MaxHeightRecyclerView extends RecyclerView {
    private int mMaxHeight;

    public MaxHeightRecyclerView(@NonNull Context context) {
        super(context);
    }

    public MaxHeightRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mMaxHeight != 0){
            super.onMeasure(widthSpec, View.MeasureSpec.makeMeasureSpec(mMaxHeight, View.MeasureSpec.AT_MOST));
        }else {
            super.onMeasure(widthSpec, heightSpec);
        }
    }

    public void setMaxHeight(int maxHeight) {
        this.mMaxHeight = maxHeight;
    }
}

2、自定义NestedScrollView,使其给RecyclerView设置最大高度

public class LazyNestedScrollView extends androidx.core.widget.NestedScrollView{
    private int mRecyclerViewId;
    private MaxHeightRecyclerView mRecyclerView;

    public LazyNestedScrollView(@NonNull Context context) {
        super(context);
    }

    public LazyNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LazyNestedScrollView);
        //获取嵌套RecyclerView控件的id
        mRecyclerViewId = typedArray.getResourceId(R.styleable.LazyNestedScrollView_recyclerview_id, 0);
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //在super前给RecyclerView设置最大值,然后通过super进行测量即可
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (mRecyclerView != null){
            mRecyclerView.setMaxHeight(height);
        }else {
            findViewById(this);
            if (mRecyclerView != null){
                mRecyclerView.setMaxHeight(height);
            }
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    //根据id递归查询RecyclerView
    private void findViewById(View view){
        if (view instanceof ViewGroup && !(view instanceof RecyclerView)){
            ViewGroup viewGroup = (ViewGroup) view;
            int childCount = viewGroup.getChildCount();
            for (int i = 0; i < childCount; i++){
                View childView = viewGroup.getChildAt(i);
                findViewById(childView);
            }
        }else {
            if (view.getId() == mRecyclerViewId && view instanceof MaxHeightRecyclerView){
                mRecyclerView = (MaxHeightRecyclerView) view;
            }
        }
    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        if (mRecyclerView != null){
            View child = getChildAt(0);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            //获取自己能够滑动的距离
            int topHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin - getMeasuredHeight();
            int scrollY = getScrollY();
            boolean topIsShow = scrollY  >=0 && scrollY  < topHeight;
            if(topIsShow) {
                //由自己响应滑动
                int remainScrollY= topHeight - scrollY;
                int selfScrollY = remainScrollY > dy ? dy : remainScrollY;
                scrollBy(0, selfScrollY);
                //告诉RecyclerView,自己滑动了多少距离
                consumed[1] = selfScrollY;
            } else {
                super.onNestedPreScroll(target, dx, dy, consumed, type);
            }
        }else {
            super.onNestedPreScroll(target, dx, dy, consumed, type);
        }
    }
}

注:

(1)RecyclerView的setNestedScrollingEnabled()应为true

(2)嵌套滑动是通过NestedScrollingParent3和NestedScrollingChild3来实现的,这里RecyclerView已经继承NestedScrollingChild3而NestedScrollView也已继承NestedScrollingParent3,RecyclerView先接收滑动事件然后先询问NestedScrollView来滑动(即onNestedPreScroll方法)然后将其滑动距离告诉RecyclerView,RecyclerView再对剩下的距离进行滑动

3、惯性滑动(NestedScrollView滑动到底部,将其滑动速度转化成惯性距离,计算子控件应滑距离=父惯性距离-父已滑距离,将子控件应滑距离转化成速度交给子控件进行惯性滑动)

(1)记录NestedScrollView惯性速度

@Override
public void fling(int velocityY) {
    super.fling(velocityY);
    mVelocityY = velocityY;
}

(2)将剩余的惯性速度传递给RecyclerView

@Override
    protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
        if (mRecyclerView != null){
            View child = getChildAt(0);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            //判断是否滑动到底部
            if (scrollY == child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin - getMeasuredHeight()){
                if (mVelocityY > 0){
                    //将惯性速度转化为滑动距离
                    double distance = FlingHelper.getInstance(getContext()).getSplineFlingDistance(mVelocityY);
                    if (distance > scrollY){
                        //将剩余滑动距离转化为惯性速度
                        int velocityY = FlingHelper.getInstance(getContext()).getVelocityByDistance(distance - scrollY);
                        //将剩余惯性速度传递给RecyclerView
                        mRecyclerView.fling(0, velocityY);
                        //重置惯性速度
                        mVelocityY = 0;
                    }
                }
            }
        }
    }

(3)惯性速度和滑动距离转化工具类

public class FlingHelper {
    private static FlingHelper mFlingHelper;
    private static final double DECELERATION_RATE = Math.log(0.78) / Math.log(0.9);
    private float mFlingFriction = ViewConfiguration.getScrollFriction();
    private float mPhysicalCoeff;
    
    private FlingHelper(Context context){
        mPhysicalCoeff = context.getResources().getDisplayMetrics().density * 160.0f * 386.0878f * 0.84f;
    }

    public static FlingHelper getInstance(Context context){
        if (mFlingHelper == null){
            mFlingHelper = new FlingHelper(context);
        }
        return mFlingHelper;
    }

    public double getSplineFlingDistance(int i){
        return Math.exp(getSplineDeceleration(i) * (DECELERATION_RATE / (DECELERATION_RATE - 1.0))) * (mFlingFriction * mPhysicalCoeff);
    }

    private double getSplineDeceleration(int i){
        return Math.log(0.35f * Math.abs(i) / (mFlingFriction * mPhysicalCoeff));
    }

    public int getVelocityByDistance(double d){
        return (int) Math.abs((Math.exp(getSplineDecelerationByDistance(d)) * mFlingFriction * mPhysicalCoeff / 0.3499999940395355));
    }

    private double getSplineDecelerationByDistance(double d){
        return (DECELERATION_RATE - 1.0) * Math.log(d / (mFlingFriction * mPhysicalCoeff)) / DECELERATION_RATE;
    }
}