NestedScrollingParent, NestedScrollingChild  详解

本文详细解析了Android中嵌套滑动机制的工作原理,包括NestedScrollingChild和NestedScrollingParent接口的作用,以及相关辅助类的使用方法。
NestedScrollingParent
NestedScrollingChild
这是两个接口,  Android 就是通过这两个接口, 来实现 子View 与父View 之间的嵌套滑动

这样的嵌套滑动机制是在 Android 发布 Lollipop 之后提供的 
不过同样在Support v7 中同样支持了 

同时 RecycleView  以及 Android 5.0 以上的系统原声View 大部分都已经支持 嵌套滑动了 

ok 了解个大概  下面来看看 具体的嵌套滑动 是怎样的:
想要理解 嵌套滑动
必须, 需要理解一下几个类(接口):
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper


先来看  NestedScrollingChild 接口,  顾名思义, 这个是子View 应该实现 的接口:

先看源码 

public interface NestedScrollingChild {

    /**
     * 设置嵌套滑动是否可用
     *
     * @param enabled
     */
    public void setNestedScrollingEnabled(boolean enabled);

    /**
     * 嵌套滑动是否可用
     *
     * @return
     */
    public boolean isNestedScrollingEnabled();

    /**
     * 开始嵌套滑动,
     *
     * @param axes 表示方向 有一下两种值
     *             ViewCompat.SCROLL_AXIS_HORIZONTAL 横向哈东
     *             ViewCompat.SCROLL_AXIS_VERTICAL 纵向滑动
     */
    public boolean startNestedScroll(int axes);

    /**
     * 停止嵌套滑动
     */
    public void stopNestedScroll();

    /**
     * 是否有父View 支持 嵌套滑动,  会一层层的网上寻找父View
     * @return
     */
    public boolean hasNestedScrollingParent();

    /**
     * 在处理滑动之后 调用
     * @param dxConsumed x轴上 被消费的距离
     * @param dyConsumed y轴上 被消费的距离
     * @param dxUnconsumed x轴上 未被消费的距离
     * @param dyUnconsumed y轴上 未被消费的距离
     * @param offsetInWindow view 的移动距离
     * @return
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    /**
     * 一般在滑动之前调用, 在ontouch 中计算出滑动距离, 然后 调用改 方法, 就给支持的嵌套的父View 处理滑动事件
     * @param dx x 轴上滑动的距离, 相对于上一次事件, 不是相对于 down事件的 那个距离
     * @param dy y 轴上滑动的距离
     * @param consumed 一个数组, 可以传 一个空的 数组,  表示 x 方向 或 y 方向的事件 是否有被消费
     * @param offsetInWindow   支持嵌套滑动到额父View 消费 滑动事件后 导致 本 View 的移动距离
     * @return 支持的嵌套的父View 是否处理了 滑动事件
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    /**
     *
     * @param velocityX x 轴上的滑动速度
     * @param velocityY y 轴上的滑动速度
     * @param consumed 是否被消费
     * @return
     */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
     *
     * @param velocityX x 轴上的滑动速度
     * @param velocityY y 轴上的滑动速度
     * @return
     */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

去掉了 原来的注释, 加入点自己理解的注释 

在看看  NestedScrollingParentHelper 这个类,
这个类是一个辅助类,  先来看看 子View 如何继承 NestedScrollingChild 

public class Child extends LinearLayout implements android.support.v4.view.NestedScrollingChild {
    public static final String TAG = "Child";

    private NestedScrollingChildHelper mNestedScrollingChildHelper;

    public Child(Context context, AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mNestedScrollingChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mNestedScrollingChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mNestedScrollingChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mNestedScrollingChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
}

可以看到基本接口里面的每个方法 都只要调用 mNestedScrollingChildHelper 中相应的方法;
下面来看看  NestedScrollingParentHelper 源码: 
public class NestedScrollingChildHelper {
    /**
     * 嵌套滑动的ziView
     */
    private final View mView;

    /**
     * 支持 嵌套滑动的 父View
     */
    private ViewParent mNestedScrollingParent;

    /**
     * 是否支持 嵌套滑动
     */
    private boolean mIsNestedScrollingEnabled;

    /**
     * 是否被消费的一个中变变量
     */
    private int[] mTempNestedScrollConsumed;

    public NestedScrollingChildHelper(View view) {
        mView = view;
    }

    public void setNestedScrollingEnabled(boolean enabled) {
        if (mIsNestedScrollingEnabled) {
            ViewCompat.stopNestedScroll(mView);
        }
        mIsNestedScrollingEnabled = enabled;
    }

    public boolean isNestedScrollingEnabled() {
        return mIsNestedScrollingEnabled;
    }

    public boolean hasNestedScrollingParent() {
        return mNestedScrollingParent != null;
    }

    /**
     * 开始嵌套滑动
     * @param axes 滑动方向
     * @return 是否有父view 支持嵌套滑动
     */
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // 如果已经找到 了嵌套滑动的父View
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            // 递归向上寻找 支持 嵌套滑动的父View
            while (p != null) {
                // 这里会调用 父View 的NestedScrollingParent.onStartNestedScroll 方法
                // 如果 父View 返回 false  则再次向上寻找父View , 直到找到支持的fuView
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    // 这里回调 父View 的onNestedScrollAccepted 方法 表示开始接收 嵌套滑动
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        // 没有找到 支持嵌套滑动的父View  则返回false
        return false;
    }

    /**
     * 停止 嵌套滑动, 一般 在 cancel up 事件中 调用
     */
    public void stopNestedScroll() {
        if (mNestedScrollingParent != null) {
            ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
            mNestedScrollingParent = null;
        }
    }

    /**
     *
     * @param dxConsumed  x 上被消费的距离
     * @param dyConsumed  y 上被消费的距离
     * @param dxUnconsumed  x 上未被消费的距离
     * @param dyUnconsumed  y 上未被消费的距离
     * @param offsetInWindow  子View 位置的移动距离
     * @return
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                // 父View 回调 onNestedScroll 方法, 该放在 主要会处理  dxUnconsumed dyUnconsumed 数据
                ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed);

                if (offsetInWindow != null) {
                    // 计算 子View的移动距离
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

    /**
     *
     * consumed[0]  为0 时 表示 x 轴方向上事件 没有被消费
     *              不为0 时 表示 x 轴方向上事件 被消费了, 值表示 被消费的滑动距离
     * consumed[1]  为0 时 表示 y 轴方向上事件 没有被消费
     *              不为0 时 表示 y 轴方向上事件 被消费了, 值表示 被消费的滑动距离
     *
     *
     * @param dx
     * @param dy
     * @param consumed
     * @param offsetInWindow
     * @return
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                // 获取 当前View 初始位置
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                // 初始化是否被消费数据
                if (consumed == null) {
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                consumed[0] = 0;
                consumed[1] = 0;

                // 这里回调 父View 的 onNestedPreScroll 方法,
                // 父View 或许会处理 相应的滑动事件,
                // 如果 处理了 则 consumed 会被赋予 相应的值
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                if (offsetInWindow != null) {
                    // 父View 处理了相应的滑动,  很可能导致 子View 的位置的移动
                    // 这里计算出  父view 消费 滑动事件后,  导致 子View 的移动距离
                    mView.getLocationInWindow(offsetInWindow);
                    // 这里 子View 的移动距离
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                // 如果  xy 方向 上 有不为0 的表示消费了 则返回true
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
                    velocityY, consumed);
        }
        return false;
    }

    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
                    velocityY);
        }
        return false;
    }

    public void onDetachedFromWindow() {
        ViewCompat.stopNestedScroll(mView);
    }

    public void onStopNestedScroll(View child) {
        ViewCompat.stopNestedScroll(mView);
    }
}

从上面的注释 可以基本看到 嵌套滑动的基本逻辑:

下面来看看 父View 
public class Parent extends LinearLayout implements NestedScrollingParent {
    public static final String TAG = "Parent";

    private NestedScrollingParentHelper mNestedScrollingParentHelper;

    public Parent(Context context, AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    /**
     * 回调开始滑动
     * @param child 该父VIew 的子View
     * @param target 支持嵌套滑动的 VIew
     * @param nestedScrollAxes 滑动方向
     * @return 是否支持 嵌套滑动
     */
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return true;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
    }

    /**
     * 这里 主要处理 dyUnconsumed dxUnconsumed 这两个值对应的数据
     * @param target
     * @param dxConsumed
     * @param dyConsumed
     * @param dxUnconsumed
     * @param dyUnconsumed
     */
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        LogUtil.d(TAG, "onNestedScroll target = " + target + " , dxConsumed = " + dxConsumed + " , dyConsumed = " + dyConsumed + " , dxUnconsumed = " + dxUnconsumed + " , dyUnconsumed = " + dyUnconsumed);
    }

    /**
     * 这里 传来了 x y 方向上的滑动距离
     * 并且 先与 子VIew  处理滑动,  并且 consumed  中可以设置相应的 除了的距离
     * 然后 子View  需要更具这感觉, 来处理自己滑动
     *
     * @param target
     * @param dx
     * @param dy
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

        consumed[1] = dy;
        LogUtil.d(TAG, "onNestedPreScroll dx = " + dx + " dy = " + dy);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }
}


总结一下  整个嵌套滑动的流程是:

子view父view
startNestedScrollonStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScrollonNestedPreScroll
dispatchNestedScrollonNestedScroll
stopNestedScrollonStopNestedScroll

通过以上的代码 注释 基本对 嵌套互动有了一个大致的基本了解

具体的还需要通过实战Demo,  这个后续在跟进吧 


评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值