嵌套滑动机制

NestedScrollingParent 与NestedScrollingChild接口说明

NestedScrollingParent

   /**
     * 有嵌套滑动到来了,判断父控件是否接受嵌套滑动
     *
     * @param child            嵌套滑动对应的父类的子类(因为嵌套滑动对于的父控件不一定是一级就能找到的,可能挑了两级父控件的父控件,child的辈分>=target)
     * @param target           具体嵌套滑动的那个子类
     * @param nestedScrollAxes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
     * @return 父控件是否接受嵌套滑动, 只有接受了才会执行剩下的嵌套滑动方法
     */
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {}

    /**
     * 当onStartNestedScroll返回为true时,也就是父控件接受嵌套滑动时,该方法才会调用
     */
    public void onNestedScrollAccepted(View child, View target, int axes) {}

    /**
     * 在嵌套滑动的子控件未滑动之前,判断父控件是否优先与子控件处理(也就是父控件可以先消耗,然后给子控件消耗)
     *
     * @param target   具体嵌套滑动的那个子类
     * @param dx       水平方向嵌套滑动的子控件想要变化的距离 dx<0 向右滑动 dx>0 向左滑动
     * @param dy       垂直方向嵌套滑动的子控件想要变化的距离 dy<0 向下滑动 dy>0 向上滑动
     * @param consumed 这个参数要我们在实现这个函数的时候指定,回头告诉子控件当前父控件消耗的距离
     *                 consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子控件做出相应的调整
     */
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {}

    /**
     * 嵌套滑动的子控件在滑动之后,判断父控件是否继续处理(也就是父消耗一定距离后,子再消耗,最后判断父消耗不)
     *
     * @param target       具体嵌套滑动的那个子类
     * @param dxConsumed   水平方向嵌套滑动的子控件滑动的距离(消耗的距离)
     * @param dyConsumed   垂直方向嵌套滑动的子控件滑动的距离(消耗的距离)
     * @param dxUnconsumed 水平方向嵌套滑动的子控件未滑动的距离(未消耗的距离)
     * @param dyUnconsumed 垂直方向嵌套滑动的子控件未滑动的距离(未消耗的距离)
     */
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}

    /**
     * 嵌套滑动结束
     */
    public void onStopNestedScroll(View child) {}

    /**
     * 当子控件产生fling滑动时,判断父控件是否处拦截fling,如果父控件处理了fling,那子控件就没有办法处理fling了。
     *
     * @param target    具体嵌套滑动的那个子类
     * @param velocityX 水平方向上的速度 velocityX > 0  向左滑动,反之向右滑动
     * @param velocityY 竖直方向上的速度 velocityY > 0  向上滑动,反之向下滑动
     * @return 父控件是否拦截该fling
     */
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {}


    /**
     * 当父控件不拦截该fling,那么子控件会将fling传入父控件
     *
     * @param target    具体嵌套滑动的那个子类
     * @param velocityX 水平方向上的速度 velocityX > 0  向左滑动,反之向右滑动
     * @param velocityY 竖直方向上的速度 velocityY > 0  向上滑动,反之向下滑动
     * @param consumed  子控件是否可以消耗该fling,也可以说是子控件是否消耗掉了该fling
     * @return 父控件是否消耗了该fling
     */
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {}

    /**
     * 返回当前父控件嵌套滑动的方向,分为水平方向与,垂直方法,或者不变
     */
    public int getNestedScrollAxes() {}

NestedScrollingChild

   /**
     * 开启一个嵌套滑动
     *
     * @param axes 支持的嵌套滑动方法,分为水平方向,竖直方向,或不指定
     * @return 如果返回true, 表示当前子控件已经找了一起嵌套滑动的view
     */
    public boolean startNestedScroll(int axes) {}

    /**
     * 在子控件滑动前,将事件分发给父控件,由父控件判断消耗多少
     *
     * @param dx             水平方向嵌套滑动的子控件想要变化的距离 dx<0 向右滑动 dx>0 向左滑动
     * @param dy             垂直方向嵌套滑动的子控件想要变化的距离 dy<0 向下滑动 dy>0 向上滑动
     * @param consumed       子控件传给父控件数组,用于存储父控件水平与竖直方向上消耗的距离,consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离
     * @param offsetInWindow 子控件在当前window的偏移量
     * @return 如果返回true, 表示父控件已经消耗了
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {}


    /**
     * 当父控件消耗事件后,子控件处理后,又继续将事件分发给父控件,由父控件判断是否消耗剩下的距离。
     *
     * @param dxConsumed     水平方向嵌套滑动的子控件滑动的距离(消耗的距离)
     * @param dyConsumed     垂直方向嵌套滑动的子控件滑动的距离(消耗的距离)
     * @param dxUnconsumed   水平方向嵌套滑动的子控件未滑动的距离(未消耗的距离)
     * @param dyUnconsumed   垂直方向嵌套滑动的子控件未滑动的距离(未消耗的距离)
     * @param offsetInWindow 子控件在当前window的偏移量
     * @return 如果返回true, 表示父控件又继续消耗了
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {}

    /**
     * 子控件停止嵌套滑动
     */
    public void stopNestedScroll() {}


    /**
     * 当子控件产生fling滑动时,判断父控件是否处拦截fling,如果父控件处理了fling,那子控件就没有办法处理fling了。
     *
     * @param velocityX 水平方向上的速度 velocityX > 0  向左滑动,反之向右滑动
     * @param velocityY 竖直方向上的速度 velocityY > 0  向上滑动,反之向下滑动
     * @return 如果返回true, 表示父控件拦截了fling
     */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {}

    /**
     * 当父控件不拦截子控件的fling,那么子控件会调用该方法将fling,传给父控件进行处理
     *
     * @param velocityX 水平方向上的速度 velocityX > 0  向左滑动,反之向右滑动
     * @param velocityY 竖直方向上的速度 velocityY > 0  向上滑动,反之向下滑动
     * @param consumed  子控件是否可以消耗该fling,也可以说是子控件是否消耗掉了该fling
     * @return 父控件是否消耗了该fling
     */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {}

    /**
     * 设置当前子控件是否支持嵌套滑动,如果不支持,那么父控件是不能够响应嵌套滑动的
     *
     * @param enabled true 支持
     */
    public void setNestedScrollingEnabled(boolean enabled) {}

    /**
     * 当前子控件是否支持嵌套滑动
     */
    public boolean isNestedScrollingEnabled() {}

    /**
     * 判断当前子控件是否拥有嵌套滑动的父控件
     */
    public boolean hasNestedScrollingParent() {}

滑动嵌套child和parent方法的调用流程

一个模版类型创建NestedScrollChild代码

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;

import androidx.annotation.Nullable;
import androidx.core.view.NestedScrollingChild;
import androidx.core.view.NestedScrollingChildHelper;
import androidx.core.view.ViewCompat;

/**
 * <pre>
 *    author : heyueyang
 *    time   : 2021/11/04
 *    desc   :
 *    version: 1.0
 */
public class NestedScrollingChildView extends View implements NestedScrollingChild {

    private NestedScrollingChildHelper mScrollingChildHelper = new NestedScrollingChildHelper(this);

    private int mMinFlingVelocity;
    private int mMaxFlingVelocity;

    public NestedScrollingChildView(Context context) {
        super(context);
        init(context);
    }

    public NestedScrollingChildView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public NestedScrollingChildView(Context context, @Nullable AttributeSet attrs,
                                    int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        ViewConfiguration vc = ViewConfiguration.get(context);
        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
    }

    /**
     * 开启一个嵌套滑动
     *
     * @param axes 支持的嵌套滑动方法,分为水平滑动,竖向滑动,或不确定
     * @return 如果返回true,表示当前子view已经知道了一起嵌套滑动的view
     */
    @Override
    public boolean startNestedScroll(int axes) {
        return mScrollingChildHelper.startNestedScroll(axes);
    }

    /**
     * 在子view滑动前,将事件分发给父view,由父view判断消耗多少
     *
     * @param dx             水平方向嵌套滑动的子View想要变化的距离 dx<0 向右滑动 dx>0 向左滑动
     * @param dy             垂直方向嵌套滑动的子View想要变化的距离 dy<0 向下滑动 dy>0 向上滑动
     * @param consumed       子view传给父view数组,用于存储父view水平与竖直方向上消耗的距离,consumed[0]
     *                       水平消耗的距离,consumed[1] 垂直消耗的距离
     * @param offsetInWindow 子view在当前window的偏移量
     * @return 如果返回true, 表示父view已经消耗了
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
                                           @Nullable int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    /**
     * 当父view消耗事件后,子view处理后,又继续将事件分发给父view,由父view判断是否消耗剩下的距离。
     *
     * @param dxConsumed     水平方向嵌套滑动的子View滑动的距离(消耗的距离)
     * @param dyConsumed     垂直方向嵌套滑动的子View滑动的距离(消耗的距离)
     * @param dxUnconsumed   水平方向嵌套滑动的子View未滑动的距离(未消耗的距离)
     * @param dyUnconsumed   垂直方向嵌套滑动的子View未滑动的距离(未消耗的距离)
     * @param offsetInWindow 子view在当前window的偏移量
     * @return 如果返回true, 表示父view又继续消耗了
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, @Nullable int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed,
                dyUnconsumed, offsetInWindow);
    }

    /**
     * 子view停止嵌套滑动
     */
    @Override
    public void stopNestedScroll() {
        mScrollingChildHelper.stopNestedScroll();
    }

    /**
     * 当子view产生fling滑动时,判断父view是否处拦截fling,如果父View处理了fling,那子view就没有办法处理fling了。
     *
     * @param velocityX 水平方向上的速度 velocityX > 0  向左滑动,反之向右滑动
     * @param velocityY 竖直方向上的速度 velocityY > 0  向上滑动,反之向下滑动
     * @return 如果返回true, 表示父view拦截了fling
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    /**
     * 当父view不拦截子view的fling,那么子view会调用该方法将fling,传给父view进行处理
     *
     * @param velocityX 水平方向上的速度 velocityX > 0  向左滑动,反之向右滑动
     * @param velocityY 竖直方向上的速度 velocityY > 0  向上滑动,反之向下滑动
     * @param consumed  子view是否可以消耗该fling,也可以说是子view是否消耗掉了该fling
     * @return 父view是否消耗了该fling
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    /**
     * 设置当前子view是否支持嵌套滑动,如果不支持,那么父view是不能够响应嵌套滑动的
     *
     * @param enabled true 支持
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    /**
     * 当前子view是否支持嵌套滑动
     */
    @Override
    public boolean isNestedScrollingEnabled() {
        return mScrollingChildHelper.isNestedScrollingEnabled();
    }

    /**
     * 判断当前子view是否拥有嵌套滑动的父view
     */
    @Override
    public boolean hasNestedScrollingParent() {
        return mScrollingChildHelper.hasNestedScrollingParent();
    }

    private int mLastY;
    private int mLastX;
    private final int[] mScrollConsumed = new int[2];
    private final int[] mScrollOffset = new int[2];
    private VelocityTracker mVelocityTracker;


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int action = event.getActionMasked();
        int y = (int) event.getY();
        int x = (int) event.getX();

        //添加速度检查器,用于处理fling效果
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = x;
                mLastY = y;
                //自己的处理逻辑,判断传递竖直还是水平方向,这里默认是设置的竖直方向
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int dy = mLastY - y;
                int dx = mLastX - x;
                //将事件传递给父控件,并记录父控件消耗的距离
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    scrollNested(dx, dy);
                }
                //如果找不到嵌套滑动的父控件,自己就处理事件
                childScroll(dx, dy);
                break;
            }
            case MotionEvent.ACTION_UP: {
                //当手指抬起的时,结束嵌套滑动传递,并判断是否产生了fling效果
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                int xvel = (int) mVelocityTracker.getXVelocity();
                int yvel = (int) mVelocityTracker.getYVelocity();
                fling(xvel, yvel);
                mVelocityTracker.clear();
                stopNestedScroll();
                break;
            }
            case MotionEvent.ACTION_CANCEL: {
                stopNestedScroll();
                mVelocityTracker.clear();
                break;
            }
        }
        return super.onTouchEvent(event);
    }

    /**
     * 子控件处理事件,并将未处理完的事件传递给父控件
     *
     * @param x 水平方向移动距离
     * @param y 竖直方向移动距离
     */
    private void scrollNested(int x, int y) {
        int unConsumedX = 0, unConsumedY = 0;
        int consumedX = 0, consumedY = 0;

        //子控件消耗多少事件,由自己决定
        if (x != 0) {
            consumedX = childConsumeX(x);
            unConsumedX = x - consumedX;
        }
        if (y != 0) {
            consumedY = childConsumeY(y);
            unConsumedY = y - consumedY;
        }

        //子控件处理事件
        childScroll(consumedX, consumedY);

        //子控件处理后,又将剩下的事件传递给父控件
        if (!dispatchNestedScroll(consumedX, consumedY, unConsumedX, unConsumedY, mScrollOffset)) {
            //传给父控件处理后,剩下的逻辑自己实现
        }
        //传递给父控件,父控件不处理,那么子控件就继续处理。
        childScroll(unConsumedX, unConsumedY);

    }

    /**
     * 子控件滑动逻辑
     */
    private void childScroll(int x, int y) {
        //子控件怎么滑动,自己实现
    }


    /**
     * 子控件水平方向消耗多少距离
     */
    private int childConsumeX(int x) {
        //具体逻辑由自己实现
        return 0;
    }

    /**
     * 子控件竖直方向消耗距离
     */
    private int childConsumeY(int y) {
        //具体逻辑由自己实现
        return 0;
    }

    private boolean fling(int velocityX, int velocityY) {
        //判断速度是否足够大。如果够大才执行fling
        if (Math.abs(velocityX) < mMinFlingVelocity) {
            velocityX = 0;
        }
        if (Math.abs(velocityY) < mMinFlingVelocity) {
            velocityY = 0;
        }
        if (velocityX == 0 && velocityY == 0) {
            return false;
        }
        if (dispatchNestedPreFling(velocityX, velocityY)) {
            boolean consumed = canScroll();
            //将fling效果传递给父控件
            dispatchNestedFling(velocityX, velocityY, consumed);
            //然后子控件在处理fling效果
            childFling();

        }
        return false;

    }


    /**
     * 判断子子控件是否能够滑动,只有能滑动才能处理fling
     */
    private boolean canScroll() {
        //具体逻辑自己实现
        return false;
    }

    /**
     * 子控件处理fling效果
     */
    private void childFling() {
        //具体逻辑自己实现
    }
}

一个Parent的实现

import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.NestedScrollingParent;
import androidx.core.view.NestedScrollingParentHelper;
import androidx.core.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import java.util.Arrays;

public class NestedScrollingParentView extends FrameLayout implements NestedScrollingParent{

    private static final String TAG = "NestedParentLayout";

    private NestedScrollingParentHelper mScrollingParentHelper;
    public NestedScrollingParentView(@NonNull Context context) {
        super(context);
    }

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

    public NestedScrollingParentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private void init() {
        mScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    /**
     * 有嵌套滑动到来了,问下该父View是否接受嵌套滑动
     *
     * @param child            嵌套滑动对应的父类的子类(因为嵌套滑动对于的父View不一定是一级就能找到的,可能挑了两级父View的父View,child的辈分>=target)
     * @param target           具体嵌套滑动的那个子类
     * @param nestedScrollAxes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
     * @return 是否接受该嵌套滑动
     */
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return true;
    }

    /**
     * 该父View接受了嵌套滑动的请求该函数调用。onStartNestedScroll返回true该函数会被调用。
     * 参数和onStartNestedScroll一样
     */
    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    /**
     * 获取嵌套滑动的轴
     *
     * @see ViewCompat#SCROLL_AXIS_HORIZONTAL 垂直
     * @see ViewCompat#SCROLL_AXIS_VERTICAL 水平
     * @see ViewCompat#SCROLL_AXIS_NONE 都支持
     */
    @Override
    public int getNestedScrollAxes() {
        return mScrollingParentHelper.getNestedScrollAxes();
    }

    /**
     * 停止嵌套滑动
     *
     * @param target 具体嵌套滑动的那个子类
     */
    @Override
    public void onStopNestedScroll(View target) {
        mScrollingParentHelper.onStopNestedScroll(target);
    }

    /**
     * 嵌套滑动的子View在滑动之后报告过来的滑动情况
     *
     * @param target       具体嵌套滑动的那个子类
     * @param dxConsumed   水平方向嵌套滑动的子View滑动的距离(消耗的距离)
     * @param dyConsumed   垂直方向嵌套滑动的子View滑动的距离(消耗的距离)
     * @param dxUnconsumed 水平方向嵌套滑动的子View未滑动的距离(未消耗的距离)
     * @param dyUnconsumed 垂直方向嵌套滑动的子View未滑动的距离(未消耗的距离)
     */
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }


    /**
     * 在嵌套滑动的子View未滑动之前告诉过来的准备滑动的情况
     *
     * @param target   具体嵌套滑动的那个子类
     * @param dx       水平方向嵌套滑动的子View想要变化的距离
     * @param dy       垂直方向嵌套滑动的子View想要变化的距离
     * @param consumed 这个参数要我们在实现这个函数的时候指定,回头告诉子View当前父View消耗的距离
     *                 consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子view做出相应的调整
     */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        final View child = target;
        if (dx > 0) {
            if (child.getRight() + dx > getWidth()) {
                dx = child.getRight() + dx - getWidth();//多出来的
                offsetLeftAndRight(dx);
                consumed[0] += dx;//父亲消耗
            }

        } else {
            if (child.getLeft() + dx < 0) {
                dx = dx + child.getLeft();
                offsetLeftAndRight(dx);
                consumed[0] += dx;//父亲消耗
            }
        }

        if (dy > 0) {
            if (child.getBottom() + dy > getHeight()) {
                dy = child.getBottom() + dy - getHeight();
                offsetTopAndBottom(dy);
                consumed[1] += dy;
            }
        } else {
            if (child.getTop() + dy < 0) {
                dy = dy + child.getTop();
                offsetTopAndBottom(dy);
                consumed[1] += dy;//父亲消耗
            }
        }
    }

    /**
     * 嵌套滑动的子View在fling之后报告过来的fling情况
     *
     * @param target    具体嵌套滑动的那个子类
     * @param velocityX 水平方向速度
     * @param velocityY 垂直方向速度
     * @param consumed  子view是否fling了
     * @return true 父View是否消耗了fling
     */
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    /**
     * 在嵌套滑动的子View未fling之前告诉过来的准备fling的情况
     *
     * @param target    具体嵌套滑动的那个子类
     * @param velocityX 水平方向速度
     * @param velocityY 垂直方向速度
     * @return true 父View是否消耗了fling
     */
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }
}

相比与NestedScrollingChild NestedScrollingChild2和NestedScrollingChild3有什么区别

NestedScrollingChild2是继承至NestedScrollingChild,它的方法上多了一个type,这个type的值包括有TYPE_TOUCH 和 TYPE_NON_TOUCH ,用这个来区分是触摸操作还是惯性滑动,这1的时没有这个值,那么就会出现当触摸事件结束,但是还有惯性滑动的时候,父控件停止滑动了,但是子view还在进行惯性的滑动。
而3相较于2只是更改了一个方法

    void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
            @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
            @NonNull int[] consumed);

方法多了一个参数consumed,这里返回了父控件消费的滑动距离