欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

从源码角度分析NestedScrolling

程序员文章站 2024-03-24 09:59:22
...

通过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,一起进步,下一篇再见~