从源码角度分析NestedScrolling

通过CoordinatorLayout可以实现许多炫酷的效果,大家可以参考我之前一篇博客:

一起玩转CoordinatorLayout

其实CoordinatorLayout就是利用NestedScrolling(嵌套滑动机制)来完成复杂的滑动交互。NestedScrolling是Android 5.0之后为我们提供的新特性,降低了使用传统事件分发机制处理嵌套滑动的难度,用于给子view与父view提供更好的交互。

今天就从源码的角度一起分析NestedScrolling,关于NestedScrolling的实现,有以下几个主要类需要关注:

NestedScrollingParent 嵌套滑动父view接口
NestedScrollingChild 嵌套滑动子view接口
NestedScrollingParentHelper 嵌套滑动父view接口的代理实现
NestedScrollingChildHelper 嵌套滑动子view接口的代理实现

我们先来看看NestedScrollingParent中的几个实现方法:

    /**
     * 父View是否允许嵌套滑动
     *
     * @param child            包含嵌套滑动父类的子View
     * @param target           实现嵌套滑动的子View
     * @param nestedScrollAxes 嵌套滑动方向,水平竖直或都支持
     */
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(child, target, nestedScrollAxes);
    }

    /**
     * onStartNestedScroll()方法返回true会调用该函数
     * 参数与onStartNestedScroll一致
     */
    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        super.onNestedScrollAccepted(child, target, axes);
    }

    /**
     * 嵌套滑动结束时调用
     *
     * @param target 实现嵌套滑动的子View
     */
    @Override
    public void onStopNestedScroll(View target) {
        super.onStopNestedScroll(target);
    }

    /**
     * 嵌套滑动子View的滑动情况(进度)
     *
     * @param target       实现嵌套滑动的子View
     * @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) {
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    /**
     * 嵌套滑动子View滑动之前的准备工作
     *
     * @param target   实现嵌套滑动的子View
     * @param dx       水平方向上嵌套滑动的子View滑动的总距离
     * @param dy       竖直方向上嵌套滑动的子View滑动的总距离
     * @param consumed consumed[0]水平方向与consumed[1]竖直方向上父View消耗(滑动)的距离
     */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(target, dx, dy, consumed);
    }

    /**
     * 嵌套滑动子View的fling(滑行)情况
     *
     * @param target    实现嵌套滑动的子View
     * @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 super.onNestedFling(target, velocityX, velocityY, consumed);
    }


    /**
     * 嵌套滑动子View fling(滑行)前的准备工作
     *
     * @param target    实现嵌套滑动的子View
     * @param velocityX 水平方向上的速度
     * @param velocityY 竖直方向上的速度
     * @return true 父View是否消耗了fling
     */
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(target, velocityX, velocityY);
    }

    /**
     * 嵌套滑动方向
     *
     * @return 水平竖直或都支持
     */
    @Override
    public int getNestedScrollAxes() {
        return super.getNestedScrollAxes();
    }

接下来看看NestedScrollingChild中的实现方法:

    /**
     * 设置是否支持嵌套滑动
     *
     * @param enabled true与false表示支持与不支持
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        super.setNestedScrollingEnabled(enabled);
    }

    /**
     * 判断嵌套滑动是否可用
     *
     * @return true表示支持嵌套滑动
     */
    @Override
    public boolean isNestedScrollingEnabled() {
        return super.isNestedScrollingEnabled();
    }

    /**
     * 开始嵌套滑动
     *
     * @param axes 方向轴,水平方向与竖直方向
     * @return
     */
    @Override
    public boolean startNestedScroll(int axes) {
        return super.startNestedScroll(axes);
    }

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

    /**
     * 判断父View是否支持嵌套滑动
     *
     * @return true与false表示支持与不支持
     */
    @Override
    public boolean hasNestedScrollingParent() {
        return super.hasNestedScrollingParent();
    }

    /**
     * 处理滑动事件
     *
     * @param dxConsumed     水平方向上消耗(滑动)的距离
     * @param dyConsumed     竖直方向上消耗(滑动)的距离
     * @param dxUnconsumed   水平方向上未消耗(未滑动)的距离
     * @param dyUnconsumed   竖直方向上未消耗(未滑动)的距离
     * @param offsetInWindow 窗体偏移量
     * @return true表示事件已经分发,false表示没有分发
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    /**
     * 处理滑动事件前的准备工作
     *
     * @param dx             水平方向上滑动的距离
     * @param dy             竖直方向上滑动的距离
     * @param consumed       父view消耗的距离
     * @param offsetInWindow 窗体偏移量
     * @return 父View是否处理了嵌套滑动
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    /**
     * fling(滑行)前的准备工作
     *
     * @param velocityX 水平方向上的速度
     * @param velocityY 竖直方向上的速度
     * @param consumed  是否被消耗
     * @return true表示被消耗,false反之
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return super.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    /**
     * fling(滑行)时调用
     *
     * @param velocityX 水平方向上的速度
     * @param velocityY 竖直方向上的速度
     * @return true表示被消耗,false反之
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return super.dispatchNestedPreFling(velocityX, velocityY);
    }

实际应用中,嵌套滑动中的父view实现NestedScrollingParent接口,嵌套滑动中的子view实现NestedScrollingChild接口。NestedScrollingParentHelper和NestedScrollingChildHelper是两个辅助类,我们只需要在对应的接口方法中调用这些辅助类的实现即可。

OK,准备工作到此结束。参考网上资料写了一个简单的例子,先看最终的效果图:

从源码角度分析NestedScrolling

最终实现的效果如上所示,通过这个实例来分析完整的嵌套滑动流程以及它们之间的分工合作。

1.子view是嵌套滑动的发起者,父view是嵌套滑动的处理者。首先在子view中允许设置嵌套滑动:

    private void init() {
        nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

2.调用startNestedScroll()方法开始嵌套滑动,并设置滑动方向:

            case MotionEvent.ACTION_DOWN: {
                mDownX = x;
                mDownY = y;
                //通知父View开始嵌套滑动,并设置滑动方向(水平竖直方向都支持)
                startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            }

这时候父view的onStartNestedScroll方法将会被回调,返回true表示允许此次嵌套滑动:

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return true;
    }

3.view开始滑动之前,会调用dispatchNestedPreScroll方法确定父view是否需要滑动。如果父view需要滑动,会消耗的距离放在consumed中,返回给子view,子view根据父view消耗的距离重新计算自己需要滑动的距离,进行滑动;如果父view不需要滑动,则子View自身处理滑动事件:

            case MotionEvent.ACTION_MOVE: {
                int dx = x - mDownX;
                int dy = y - mDownY;

                //如果父View处理滑动事件
                if (dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)) {
                    //减去父View消耗的距离
                    dx -= consumed[0];
                    dy -= consumed[1];
                }
                offsetLeftAndRight(dx);
                offsetTopAndBottom(dy);

                break;
            }

这时候父view的onNestedPreScroll方法将会被回调,协同处理滑动事件:

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(target, dx, dy, consumed);


        //向右滑动
        if (dx > 0) {
         //滑动到边界
            if (target.getRight() + dx > getWidth()) {
                dx = target.getRight() + dx - getWidth();
                //父View消耗
                offsetLeftAndRight(dx);
                consumed[0] += dx;
            }
        }
        //向左滑动
        else {
            if (target.getLeft() + dx < 0) {
                dx = dx + target.getLeft();
                //父View消耗
                offsetLeftAndRight(dx);
                consumed[0] += dx;
            }
        }
        //向下滑动
        if (dy > 0) {
            if (target.getBottom() + dy > getHeight()) {
                dy = target.getBottom() + dy - getHeight();
                //父View消耗
                offsetTopAndBottom(dy);
                consumed[1] += dy;
            }
        }
        //向上滑动
        else {
            if (target.getTop() + dy < 0) {
                dy = dy + target.getTop();
                //父View消耗
                offsetTopAndBottom(dy);
                consumed[1] += dy;
            }
        }

    }

4.子view计算完自己的滑动距离进行滑动之后,调用dispatchNestedScroll方法进行滑动:

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

5.如果需要停止嵌套滑动,子view调用stopNestedScroll方法,父view的onStopNestedScroll方法被回调结束滑动:


            case MotionEvent.ACTION_UP: {
                //结束嵌套滑动
                stopNestedScroll();
                break;
            }

至此,我们已经经历了一次完整的嵌套滑动流程,实际上内部都是通过NestedScrollingChildHelper实现的,我们只需要在恰当的地方传入参数调用方法即可。

关于NestedScrollingParentHelper源码解析可以参考下面的博客:

NestedScrollingParent,NestedScrollingParentHelper 详解

希望能对你有所帮助,源码已经同步上传到github上:

https://github.com/18722527635/AndroidArtStudy

欢迎star,fork,提issues,一起进步,下一篇再见~

上一篇:2018,你好


下一篇:调用系统相机拍照与手机图库