NestedScrolling机制阅读笔记

简述

在Android5.0之后,官方提供了一种特定的手势处理机制:嵌套滑动
其实主要的用途就是当一个可以滑动的页面中有其它可以滑动的控件的时候,希望这个控件在滑动到一定条件之后页面开始滑动,这个可能是相对来说比较常见的场景
官方为了兼容一些5.0以下的情况,在v4包中提供了NestedScrollingChildHelper和NestedScrollingParentHelper来允许自定义处理
当然像旧版本类似ListView之类的控件就不支持这套机制,一般常用的可能是SwipeRefreshLayout、RecyclerView和NestedScrollView等v4的控件

流程分析

为了更好的理解这个机制,通过一个例子的流程来分析可能会好一点
首先要找到两个官方支持的控件,这里选择了NestedScrollView和RecyclerView

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {

public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
        NestedScrollingChild, ScrollingView {

NestedScrollView默认实现NestedScrollingParent和NestedScrollingChild
RecyclerView默认实现NestedScrollingChild
接下来看一下这两个接口:

    /**
     * 一个应该被ViewGroup所实现的接口,如果你希望支持在一个支持nested scrolling的子视图中进行滑动的时候来代理的话
     *
     * 实现该接口的类应该创建一个final类型的变量NestedScrollingParentHelper,在一些处理中可以直接进行代理
     *
     * 在处理嵌套滑动的时候最好通过ViewCompat、ViewGroupCompat或者ViewParentCompat的静态方法
     * 这样可以确保在Android的不同版本的一致性
     */
    public interface NestedScrollingParent {
        /**
         * 在子视图调用startNestedScroll方法之后,这个方法会被调用
         * startNestedScroll会从当前视图向上询问那些想要处理的nested scrolling的父布局
         *
         * 这个方法应该在一个ViewGroup希望支持嵌套滑动的时候实现。
         * 如果返回true,当前ViewGroup就会成为nested scrolling parent来代理滑动过程中的一些行为。
         * 当一套nested scroll完整结束之后将会调用onStopNestedScroll来结束当前嵌套滑动
         *
         * @param child 当前可以代理嵌套滑动的父布局ViewGroup
         * @param target 当前发出嵌套滑动开始的子视图
         * @param nestedScrollAxes 滑动方向标志
         * @return true表示当前父布局可以处理嵌套滑动
         */
        public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

        /**
         * 该方法会在onStartNestedScroll返回true之后调用一次
         * 主要是提供一个机会在处理nested scroll之前进行一些配置的初始化
         *
         * @param child 当前可以代理嵌套滑动的父布局ViewGroup
         * @param target 当前发出嵌套滑动开始的子视图
         * @param nestedScrollAxes 滑动方向标志
         */
        public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

        /**
         * 该方法一般在一套完整的nested scroll完成之后调用,比方说一般在ACTION_UP和ACTION_CANCEL事件中回调
         *
         * @param target 当前发出嵌套滑动开始的子视图
         */
        public void onStopNestedScroll(View target);

        /**
         * 这个方法会在一个支持nested scrolling的子视图分发nested scroll事件的时候调用。
         * 一般可能是MOVE事件的时候分发
         * 这个方法要进行回调,首先在之前的onStartNestedScroll中要返回true
         *
         * @param target 当前发出嵌套滑动开始的子视图
         * @param dxConsumed 当前在水平方向上target已经消耗了的偏移量
         * @param dyConsumed 当前在竖直方向上target已经消耗了的偏移量
         * @param dxUnconsumed 当前在水平方向上target还没有消耗的偏移量
         * @param dyUnconsumed 当前在竖直方向上target还没有消耗的偏移量
         */
        public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                                   int dxUnconsumed, int dyUnconsumed);

        /**
         * 可以通过这个方法在当前发出嵌套滑动开始操作的子视图滑动之前先消费事件
         *
         * 在实现该方法的时候应该指明消费了多少滑动的距离,通过consumed数组,该数组一定非null,默认值都为0
         * consumed[0]表示水平方向上面消费了的距离
         * consumed[1]表示竖直方向上面消费了的距离
         *
         * @param target 当前发出嵌套滑动开始的子视图
         * @param dx 当前发出嵌套滑动开始的子视图在当前滑动中的水平偏移量
         * @param dy 当前发出嵌套滑动开始的子视图在当前滑动中的竖直偏移量
         * @param consumed 当前代理了嵌套滑动的父布局在此次滑动中消耗的偏移量
         */
        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

        /**
         * nested scroll中处理fling事件
         *
         * @param target 当前发出嵌套滑动开始的子视图
         * @param velocityX 1s内水平方向的偏移速度
         * @param velocityY 1s内竖直方向的偏移速度
         * @param consumed true表示当前发出嵌套滑动开始的子视图是否会接着进行fling操作
         * @return true表示当前父布局在当前发出嵌套滑动开始的子视图fling之前消费了fling,后续子视图还是可以继续处理fling
         */
        public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

        /**
         * 在当前发出嵌套滑动开始的子视图消费fling事件之前的处理
         *
         * 一般来说这个方法在调用之前已经通过VelocityTracker完成了fling相关的参数计算
         * 规范的说后续的velocityX/Y都会在ViewConfiguration.getScaledMinimumFlingVelocity()/ViewConfiguration.getScaledMaximumFlingVelocity()之间
         *
         * 通过返回true将会告知当前发出嵌套滑动开始的子视图不处理fling操作,相当于拦截
         *
         * @param target 当前发出嵌套滑动开始的子视图
         * @param velocityX 1s内水平方向的偏移速度
         * @param velocityY 1s内竖直方向的偏移速度
         * @return true表示当前父布局在当前发出嵌套滑动开始的子视图fling之前就完全消费了fling,会导致子视图不处理fling
         */
        public boolean onNestedPreFling(View target, float velocityX, float velocityY);

        /**
         * 返回当前NestedScrollingParent进行nested scrolling的方向
         *
         * @return Flags indicating the current axes of nested scrolling
         * @see ViewCompat#SCROLL_AXIS_HORIZONTAL 水平方向
         * @see ViewCompat#SCROLL_AXIS_VERTICAL 竖直方向
         * @see ViewCompat#SCROLL_AXIS_NONE
         */
        public int getNestedScrollAxes();
    }

    /**
     * 这个接口一般是由View实现
     * 然后结合实现NestedScrollingParent的ViewGroup来完成一组嵌套滑动
     *
     * 实现该接口的类需要创建一个final的NestedScrollingChildHelper对象
     * 然后可以将内部大多数的方法委托给该对象处理
     *
     * 尽量通过ViewCompat、ViewGroupCompat、ViewParentCompat处理逻辑,这样可以保证不同Android版本的兼容性
     */
    public interface NestedScrollingChild {
        /**
         * 允许/禁止当前视图进行嵌套滑动操作
         *
         * 如果当前视图被禁止进行嵌套滑动
         * 那么后续的类似dispatchNestedScroll等操作都会不处理
         *
         * @param enabled true的话表示允许,否则不允许,如果之前是允许状态变为不允许,那么还会发出stopNestedScroll事件
         */
        public void setNestedScrollingEnabled(boolean enabled);

        /**
         * 当前视图是否可以进行嵌套滑动操作
         * @return true表示可以
         */
        public boolean isNestedScrollingEnabled();

        /**
         * 开始指定方向的嵌套滑动
         *
         * 一般来说,视图应该在滑动行为一开始就调用startNestedScroll这个方法
         * 比方说在ACTION_DOWN这个事件的时候
         *
         * 当前方法返回true,说明找到了可以进行嵌套滑动的父布局ViewGroup
         * 否则只能等待下一次的事件序列开始再重新查找,当前的事件序列默认没有嵌套滑动
         * 假设之前已经找到了一个嵌套滑动的ViewGroup,后续会直接使用之前找到的ViewGroup来进行嵌套滑动
         *
         * 视图在滑动之前应该先调用一次dispatchNestedPreScroll来进行计算实际需要的滑动偏移量
         * 如果返回true,意味着有父布局消费了一些滑动偏移量,那么后续的滑动应该基于消费后的偏移量进行
         *
         * 接着,需要在当前视图滑动之前通过调用dispatchNestedScroll来进行滑动事件分发
         * 这个过程中可能相应嵌套滑动的ViewGroup会消耗滑动偏移量
         * 最后再剩余的滑动偏移量的基础上,再考虑是否进行当前视图的滑动
         *
         * @param axes 嵌套滑动的方向标志{@link ViewCompat#SCROLL_AXIS_HORIZONTAL}
         *             and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}.
         * @return true表示当前找到了可以进行嵌套滑动的父布局ViewGroup
         */
        public boolean startNestedScroll(int axes);

        /**
         * 停止进行中的嵌套滑动
         *
         * @see #startNestedScroll(int)
         */
        public void stopNestedScroll();

        /**
         * 当前视图是否已经找到了一个可以进行嵌套滑动的父布局ViewGroup
         */
        public boolean hasNestedScrollingParent();

        /**
         * 分发一个滑动事件
         *
         * 在View自己消费滑动事件之后,将事件告知可以进行嵌套滑动的父ViewGroup
         *
         * 如果当前不允许进行嵌套滑动或者在这之前没有找到可以进行嵌套滑动的父ViewGroup
         * 当前分发无效
         *
         * @param dxConsumed 滑动中当前视图已经消费的水平偏移量
         * @param dyConsumed 滑动中当前视图已经消费的竖直偏移量
         * @param dxUnconsumed 滑动中当前视图还没有消费的水平偏移量
         * @param dyUnconsumed 滑动中当前视图还没有消费的竖直偏移量
         * @param offsetInWindow 可选项。如果当前非null,那么会在将滑动事件分发给嵌套滑动的父ViewGroup之前和之后分别计算
         *                       一次当前视图在window的位置,然后进行做差。
         *                       这个是反映在嵌套滑动之后视图的位置变化,可以用于后续调整手指坐标
         * @return true表示滑动分发完成
         */
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

        /**
         * 在视图滑动之前提供一个机会给嵌套滑动的父布局消耗偏移量
         * 相当于进行滑动前偏移量分发
         *
         * @param dx 当前视图水平滑动的偏移量
         * @param dy 当前视图竖直滑动的偏移量
         * @param consumed 输出。如果非null,通过consumed[0]记录嵌套滑动中父布局消费的水平滑动偏移量,
         *                 通过consumed[1]记录嵌套滑动中父布局消费的竖直滑动偏移量
         * @param offsetInWindow 可选项。如果当前非null,那么会在将滑动事件分发给嵌套滑动的父ViewGroup之前和之后分别计算
         *                       一次当前视图在window的位置,然后进行做差。
         *                       这个是反映在嵌套滑动之后视图的位置变化,可以用于后续调整手指坐标
         * @return true表示当前处理嵌套滑动的父布局消耗了一些滑动偏移量
         */
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

        /**
         * 分发fling事件给处理嵌套滑动的父布局ViewGroup
         *
         * @param velocityX 1s内视图水平滑动的速度
         * @param velocityY 1s内视图竖直滑动的速度
         * @param consumed true表示当前视图将会处理fling,否则不处理
         * @return true表示当前处理嵌套滑动的父布局ViewGroup消耗了fling
         */
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

        /**
         * 在视图处理fling之前先分发fling事件给当前处理嵌套滑动的父布局ViewGroup
         *
         * 实际上这个方法的主要作用是用来拦截fling事件
         * 如果返回true的话视图将不会再处理fling
         *
         * 一般来说这个方法有两种处理逻辑:
         * 1.如果视图被分页,并且要调到指定的页的位置,那么不需要调用dispatchNestedPreFling
         * 2.如果前处理嵌套滑动的父布局ViewGroup消费了fling事件,那么当前视图则不应该继续滑动
         *
         * 视图不应该提供不支持的滑动方向的fling竖直,比方说ScrollView不应该提供水平方向的滑动速度
         *
         * @param velocityX 1s内视图水平滑动的速度
         * @param velocityY 1s内视图竖直滑动的速度
         * @return true表示当前处理嵌套滑动的父布局ViewGroup在子视图fling之前就有消耗fling
         */
        public boolean dispatchNestedPreFling(float velocityX, float velocityY);
    }

从基本的注释中可以看到,实际上NestedScrollingChild相当于嵌套滑动发起者,而 NestedScrollingParent则是嵌套滑动的处理者。
一般来说NestedScrollingChild通过委托v4包中的NestedScrollingChildHelper处理,内部完成了预设的嵌套发起相关的逻辑。
比方说看一下RecyclerView基于NestedScrollingChild接口的实现:

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

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

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

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

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

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

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

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

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

接下里继续从RecyclerView入手分析,主要看拦截事件和处理事件这两个关键逻辑:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        //...
        final int action = MotionEventCompat.getActionMasked(e);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //...
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {//水平方向滑动
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {//竖直方向滑动
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis);
                break;

            case MotionEvent.ACTION_UP: {
                //...
                stopNestedScroll();
            } break;

            case MotionEvent.ACTION_CANCEL: {
                //...
                stopNestedScroll();
            }
        }
        //...
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        //...
        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                //...
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis);
            } break;

            case MotionEventCompat.ACTION_POINTER_DOWN: {
                //....
            } break;

            case MotionEvent.ACTION_MOVE: {
                final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id " +
                            mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
                final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
                int dx = mLastTouchX - x;//当前水平滑动的偏移量
                int dy = mLastTouchY - y;//当前竖直滑动的偏移量
                //首先进入分发准备滑动阶段
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];//之后可用的水平滑动的偏移量需要减去处理嵌套滑动的父布局消费的水平偏移量
                    dy -= mScrollConsumed[1];//之后可用的竖直滑动的偏移量需要减去处理嵌套滑动的父布局消费的竖直偏移量
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }

                //...

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    //这里是实际滑动的处理
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
            } break;

            case MotionEventCompat.ACTION_POINTER_UP: {
                //...
            } break;

            case MotionEvent.ACTION_UP: {
                //先计算当前fling速度
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally ?
                        -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
                final float yvel = canScrollVertically ?
                        -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
                //这里处理fling的实际逻辑
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }

                mVelocityTracker.clear();
                releaseGlows();
            } break;

            case MotionEvent.ACTION_CANCEL: {
                //...
                stopNestedScroll();
            } break;
        }
        //...
        return true;
    }

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        //...
        if (mAdapter != null) {
            //...
            if (x != 0) {
                //这里实际上就是通过LayoutManager完成RecyclerView的水平滑动,并且从中可以计算出当前水平滑动消费的偏移量
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                //计算当前手指水平移动时还没有被消费的偏移量
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                //这里实际上就是通过LayoutManager完成RecyclerView的竖直滑动,并且从中可以计算出当前竖直滑动消费的偏移量
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                //计算当前手指竖直移动时还没有被消费的偏移量
                unconsumedY = y - consumedY;
            }
            //这里说一个简单例子就理解了,假设一个RecyclerView距离最顶部只有2px,但是本次手指move了5px
            //那么consumedX就是2px,unconsumedX就是3px
            //其实就是相当于一个常用场景,当RecyclerView滑动2px到顶部之後,其它视图可以接着拿到剩下的3px做操作
            //这就是嵌套滑动
            //...
        }
        //...
        //当前视图已经滑动完成,进行嵌套滑动的滑动分发
        //consumedX和consumedY表示本次视图滑动完成后消费的偏移量
        //unconsumedX和unconsumedY表示本次视图滑动完成后还没有消费的偏移量
        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
            // Update the last touch co-ords, taking any scroll offset into account
            mLastTouchX -= mScrollOffset[0];
            mLastTouchY -= mScrollOffset[1];
            if (ev != null) {
                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            }
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
        } else if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
            //...
        }
        //...
    }

    public boolean fling(int velocityX, int velocityY) {
        //...
        //首先进入准备分发嵌套滑动的fling事件阶段
        if (!dispatchNestedPreFling(velocityX, velocityY)) {//如果返回true,则视图不会再进行fling操作
            //首先分发嵌套滑动的fling事件
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (canScroll) {
                //...
                //这里才是进行视图本身的fling操作
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

可以看到,这就是一个标准的View对于嵌套滑动发起流程:
1.拦截事件阶段:
DOWN事件查找是否有响应嵌套滑动的父布局
UP和CANCEL事件停止嵌套滑动。
2.处理事件阶段:
DOWN事件查找是否有响应嵌套滑动的父布局。
MOVE事件首先在视图滑动前先分发滑动偏移量,然后计算剩余偏移量,然后视图根据剩余的偏移量进行滑动,接着分发嵌套滑动的滑动事件。
CANCEL事件停止嵌套滑动。
UP事件如果有fling,先分发嵌套滑动准备fling事件,如果返回true则完成,否则继续分发嵌套滑动fling事件,再接着视图本身进行fling操作。如果没有fling,一般可以直接停止嵌套滑动。
再看NestedScrollingParentHelper的实现之前,先通过NestedScrollView的实现把NestedScrollingParent的实现理解,从而理解完整的嵌套滑动机制:

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        //实际上这里就是如果子视图发出的是竖直方向上面的嵌套滑动
        //那么NestedScrollView就接受
        //这里就是用于判断接受嵌套滑动的条件
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        //当前是第一次接受了嵌套滑动请求
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        //因为NestedScrollView本身也实现NestedScrollingChild,那么在自身处理嵌套滑动之前将该事件继续向上询问
        //越顶层的视图越先处理
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    }

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

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                               int dyUnconsumed) {
        final int oldScrollY = getScrollY();
        scrollBy(0, dyUnconsumed);//这里就是根据当前发起嵌套滑动视图还没有消费的竖直偏移量进行滑动
        final int myConsumed = getScrollY() - oldScrollY;//计算当前NestedScrollView竖直滑动消费的偏移量,因为可能只消费了一部分
        final int myUnconsumed = dyUnconsumed - myConsumed;
        //将嵌套滑动继续向上发送
        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        //可以看到NestedScrollView在滑动之前不需要做什么
        //只是单纯向上发出嵌套滑动请求
        dispatchNestedPreScroll(dx, dy, consumed, null);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        if (!consumed) {//当前fling没有被子视图消费
            //那么进行NestedScrollView自身的fling操作
            flingWithNestedDispatch((int) velocityY);
            return true;
        }
        return false;
    }

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

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

可以看到,NestedScrollView作为一个即实现了NestedScrollingChild和NestedScrollingParent的视图,在处理嵌套滑动的时候,会在特定的回调中继续向上发起嵌套滑动请求。
事件分发是从视图树顶层向下分发,而嵌套滑动则是刚好相反,从接收到事件的视图开始,向视图树上面进行分发。
NestedScrollView在嵌套滑动中的处理,简单说就是你不滑了我接着滑这种表现形式。
接下来看NestedScrollingParentHelper的实现,可以看到在NestedScrollView中的回调都委托了它进行处理:

    public class NestedScrollingParentHelper {
        private final ViewGroup mViewGroup;//记录当前使用nested scrolling的ViewGroup
        private int mNestedScrollAxes;//当前ViewGroup所接受的嵌套滑动的方向

        public NestedScrollingParentHelper(ViewGroup viewGroup) {
            mViewGroup = viewGroup;
        }

        /**
         * 在实现NestedScrollingParent的onNestedScrollAccepted中使用
         */
        public void onNestedScrollAccepted(View child, View target, int axes) {
            mNestedScrollAxes = axes;//其实就是记录了当前ViewGroup接受的嵌套滑动的方向
        }

        /**
         * 返回当前ViewGroup所接受的嵌套滑动的方向
         * 
         */
        public int getNestedScrollAxes() {
            return mNestedScrollAxes;
        }

        /**
         * 在实现NestedScrollingParent的onStopNestedScroll中使用
         */
        public void onStopNestedScroll(View target) {
            mNestedScrollAxes = 0;//还原了当前可以接受的嵌套滑动的方向,等待下一次接受嵌套滑动的时候再重新赋值
        }
    }

其实就是预定义了几个方法,内部中记录了几个参数而已。
接着看比较重要的NestedScrollingChildHelper:

    public class NestedScrollingChildHelper {
        private final View mView;//当前进行嵌套滑动事件分发的视图
        private ViewParent mNestedScrollingParent;//当前已经找到的响应mView发出的嵌套滑动请求的父视图组
        private boolean mIsNestedScrollingEnabled;//当前mView是否可以使用嵌套滑动机制
        private int[] mTempNestedScrollConsumed;//一个数组,为了避免多次构建数组来存放当前消费滑动的偏移量

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

        /**
         * 是否允许mView进行嵌套滑动机制
         */
        public void setNestedScrollingEnabled(boolean enabled) {
            if (mIsNestedScrollingEnabled) {//如果之前是允许进行,然后修改状态为不允许,此时要先分发嵌套滑动停止事件
                ViewCompat.stopNestedScroll(mView);
            }
            mIsNestedScrollingEnabled = enabled;
        }

        /**
         * 当前是否允许进行嵌套滑动机制
         */
        public boolean isNestedScrollingEnabled() {
            return mIsNestedScrollingEnabled;
        }

        /**
         * 当前是否找到了响应当前视图的嵌套滑动的父视图组
         */
        public boolean hasNestedScrollingParent() {
            return mNestedScrollingParent != null;
        }

        /**
         * 通过mView发起一个新的嵌套滑动请求
         */
        public boolean startNestedScroll(int axes) {
            if (hasNestedScrollingParent()) {
                // 当前已经处于嵌套滑动中,不需要重新查找
                return true;
            }
            if (isNestedScrollingEnabled()) {//当前mView允许进行嵌套滑动机制
                ViewParent p = mView.getParent();
                View child = mView;
                while (p != null) {
                    //查找一个可以响应mView发出的嵌套滑动的父视图组
                    if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                        //成功找到响应的父视图组
                        mNestedScrollingParent = p;//记录当前响应的父视图组
                        //回调一次onNestedScrollAccepted方法
                        ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                        return true;
                    }
                    //向视图树上方进行遍历
                    if (p instanceof View) {
                        child = (View) p;
                    }
                    p = p.getParent();
                }
            }
            return false;//当前无法找到进行嵌套滑动的父视图组
        }

        /**
         * 停止嵌套滑动
         */
        public void stopNestedScroll() {
            if (mNestedScrollingParent != null) {
                ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
                mNestedScrollingParent = null;
            }
        }

        /**
         * 分发嵌套滑动的滑动事件
         */
        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];
                    }

                    ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                            dyConsumed, dxUnconsumed, dyUnconsumed);

                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                        //可以看到,offsetInWindow这个参数实际上就是分发前后mView的位置变化
                    }
                    return true;
                } else if (offsetInWindow != null) {
                    // No motion, no dispatch. Keep offsetInWindow up to date.
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
            return false;
        }

        /**
         * 分发嵌套滑动的准备滑动事件
         */
        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;
                    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;
                    ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                    }
                    return consumed[0] != 0 || consumed[1] != 0;
                } else if (offsetInWindow != null) {
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
            return false;
        }

        /**
         * 分发嵌套滑动的fling事件
         */
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
                        velocityY, consumed);
            }
            return false;
        }

        /**
         * 分发嵌套滑动的准备进行fling事件
         */
        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
                        velocityY);
            }
            return false;
        }

        /**
         * mView在onDetachedFromWindow时候应该调用该方法
         * 用于停止嵌套滑动
         */
        public void onDetachedFromWindow() {
            ViewCompat.stopNestedScroll(mView);
        }

        /**
         * 当嵌套滑动停止的时候应该调用
         */
        public void onStopNestedScroll(View child) {
            ViewCompat.stopNestedScroll(mView);
        }
    }

主要的工作系统已经完成,剩下要做的主要就是在对应的方法中进行委托即可。

结语

嵌套滑动是事件处理的一种机制,这个机制是完全基于事件分发来进行的。
当一个事件成功分发之后,一个视图获得了事件序列,那么这个视图在后续的处理中,可以尝试在这个过程中和父视图组进行联动,在一些特定的事件中让父视图组进行一些操作,这个才是嵌套滑动的意义。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容