通俗易懂的小例子来演示如何使用NestedScroll

写在前面

最近遇到了一个问题,在SwipeRefreshLayout中,有时候下拉,圆球不会下来,等松开手指的时候,球会突然闪一下,不明所以。想到这个应该是滑动相关的问题,而且跟嵌套滑动似乎很有关联,我们看,public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,NestedScrollingChild,可以看出SwipeRefreshLayout 即实现了NestedScrollingParent也实现了NestedScrollingChild,那先从这个角度着手,看看NestedScroll是个什么玩意儿。

学习一个

先来看看这两篇文章
* [Android 嵌套滑动机制(NestedScrolling)- Gemini](Android 嵌套滑动机制(NestedScrolling))
* Android NestedScrolling 实战 - [Android](http://www.race604.com/tag/android/)

这里摘抄几句关于NestedScrollingChild比较重要的:

需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用 startNestedScroll()。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用 dispatchNestedPreScroll()。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用 dispatchNestedScroll()

关于NestedScrollingParent的:

从上面的 Child 分析可知,滑动开始的调用 startNestedScroll(),Parent 收到 onStartNestedScroll()回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调 onNestedScrollAccepted()
每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll(),这就回调到 Parent 的 onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。
Child 滑动以后,会调用 onNestedScroll(),回调到 Parent 的 onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。
最后,滑动结束,调用 onStopNestedScroll()表示本次处理结束。

下面的内容是假定大家已经把上面两篇文章看完了。

我的例子

其实上面两篇文章已经写明白了,但有点不足的是,没有一个通俗易懂的例子来演示。所以如果各位还不是太清楚的话,可以通过下面的例子来理解。

先来看一个图。

这是一整次的滑动,橙色的为子View,蓝色的为父View。我们将子View往上滑的时候,先是父View带着子View一起向上滑动,等父View到了顶之后,子View开始滑动。

大概的原理是,滑动事件在子View中的时候,先让父View进行滑动的处理,然后子View去处理未被父View消费的距离。

在代码中是这么处理的。
1. 首先,子View是肯定需要实现NestedScrollingChild的,然后重写onTouchEvent方法,。。。
~~2.~~

得,不解释了。Talk is plain. Show you the codes.

下面是子View的实现。


public class NestedChildView extends View implements NestedScrollingChild {

    public static final String TAG = "NestedChildView";

    private final NestedScrollingChildHelper childHelper = new NestedScrollingChildHelper(this);
    private float downY;

    private int[] consumed = new int[2];
    private int[] offsetInWindow = new int[2];


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


    public NestedChildView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();

    }

    private void init() {
        setNestedScrollingEnabled(true);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int actionMasked = MotionEventCompat.getActionMasked(event);

        // 取第一个接触屏幕的手指Id
        final int pointerId = MotionEventCompat.getPointerId(event, 0);
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN:

                // 取得当前的Y,并赋值给lastY变量
                downY = getPointerY(event, pointerId);
                // 找不到手指,放弃掉这个触摸事件流
                if (downY == -1) {
                    return false;
                }

                // 通知父View,开始滑动
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            case MotionEvent.ACTION_MOVE:

                // 获得当前手指的Y
                final float pointerY = getPointerY(event, pointerId);

                // 找不到手指,放弃掉这个触摸事件流
                if (pointerY == -1) {
                    return false;
                }

                // 计算出滑动的偏移量
                float deltaY = pointerY - downY;

                Log.d(TAG, String.format("downY = %f",deltaY));

                Log.d(TAG, String.format("before dispatchNestedPreScroll, deltaY = %f", deltaY));

                // 通知父View, 子View想滑动 deltaY 个偏移量,父View要不要先滑一下,然后把父View滑了多少,告诉子View一下
                // 下面这个方法的前两个参数为在x,y方向上想要滑动的偏移量
                // 第三个参数为一个长度为2的整型数组,父View将消费掉的距离放置在这个数组里面
                // 第四个参数为一个长度为2的整型数组,父View在屏幕里面的偏移量放置在这个数组里面
                // 返回值为 true,代表父View有消费任何的滑动.
                if (dispatchNestedPreScroll(0, (int) deltaY, consumed, offsetInWindow)) {

                    // 偏移量需要减掉被父View消费掉的
                    deltaY -= consumed[1];
                    Log.d(TAG, String.format("after dispatchNestedPreScroll , deltaY = %f", deltaY));

                }

                // 上面的 (int)deltaY 会造成精度丢失,这里把精度给舍弃掉
                if(Math.floor(Math.abs(deltaY)) == 0) {
                    deltaY = 0;
                }

                // 这里移动子View,下面的min,max是为了控制边界,避免子View越界
                setY(Math.min(Math.max(getY() + deltaY, 0), ((View) getParent()).getHeight() - getHeight()));


                break;
        }
        return true;
    }

    /**
     * 这个方法通过pointerId获取pointerIndex,然后获取Y
     *
     */
    private float getPointerY(MotionEvent event, int pointerId) {
        final int pointerIndex = MotionEventCompat.findPointerIndex(event, pointerId);
        if (pointerIndex < 0) {
            return -1;
        }
        return MotionEventCompat.getY(event, pointerIndex);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        Log.d(TAG, String.format("setNestedScrollingEnabled , enabled = %b", enabled));
        childHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        Log.d(TAG, "isNestedScrollingEnabled");
        return childHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        Log.d(TAG, String.format("startNestedScroll , axes = %d", axes));
        return childHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        Log.d(TAG, "stopNestedScroll");
        childHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        Log.d(TAG, "hasNestedScrollingParent");
        return childHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        final boolean b = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
        Log.d(TAG, String.format("dispatchNestedScroll , dxConsumed = %d, dyConsumed = %d, dxUnconsumed = %d, dyUnconsumed = %d, offsetInWindow = %s", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, Arrays.toString(offsetInWindow)));
        return b;
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        final boolean b = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
        Log.d(TAG, String.format("dispatchNestedPreScroll , dx = %d, dy = %d, consumed = %s, offsetInWindow = %s", dx, dy, Arrays.toString(consumed), Arrays.toString(offsetInWindow)));
        return b;
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        Log.d(TAG, String.format("dispatchNestedFling , velocityX = %f, velocityY = %f, consumed = %b", velocityX, velocityY, consumed));
        return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        Log.d(TAG, String.format("dispatchNestedPreFling , velocityX = %f, velocityY = %f", velocityX, velocityY));
        return childHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
}

可以看到,NestedScrollingChild接口中的方法,都委托给NestedScrollingChildHelper去实现了,根本就不用我们来做。其实在Lollipop版本以上,View中是有这些方法的,只是我们要兼容Lollipop以下的版本,所以要自己来实现这个接口。

主要的逻辑,就在onTouchEvent方法中了。如果之前有重写过这个方法的经验,其实一点都不复杂。
1. 在ACTION_DOWN中,记录了一个按下的位置。
2. 在ACTION_MOVE中,计算出偏移量,然后将这个偏移量,通过dispatchNestedPreScroll方法,传递给父View(当然,是需要实现NestedScrollingParent的父View),稍后会贴出父View中,在收到通知后,是怎么处理的。
3. 如果被有被父View消费,那么偏移量需要减去被父View消费掉的。
4. 根据偏移量移动子View。


下面看父View是怎么实现的。

public class NestedParentView extends FrameLayout implements NestedScrollingParent {

    public static final String TAG = NestedParentView.class.getSimpleName();

    private NestedScrollingParentHelper parentHelper;

    public NestedParentView(Context context) {
        super(context);
    }

    public NestedParentView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    {
        parentHelper = new NestedScrollingParentHelper(this);

    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.d(TAG, String.format("onStartNestedScroll, child = %s, target = %s, nestedScrollAxes = %d", child, target, nestedScrollAxes));
        return true;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        Log.d(TAG, String.format("onNestedScrollAccepted, child = %s, target = %s, nestedScrollAxes = %d", child, target, nestedScrollAxes));
        parentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        Log.d(TAG, "onStopNestedScroll");
        parentHelper.onStopNestedScroll(target);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        Log.d(TAG, String.format("onNestedScroll, dxConsumed = %d, dyConsumed = %d, dxUnconsumed = %d, dyUnconsumed = %d", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed));
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        // 应该移动的Y距离
        final float shouldMoveY = getY() + dy;

        // 获取到父View的容器的引用,这里假定父View容器是View
        final View parent = (View) getParent();

        int consumedY;
        // 如果超过了父View的上边界,只消费子View到父View上边的距离
        if (shouldMoveY <= 0) {
            consumedY = - (int) getY();
        } else if (shouldMoveY >= parent.getHeight() - getHeight()) {
            // 如果超过了父View的下边界,只消费子View到父View
            consumedY = (int) (parent.getHeight() - getHeight() - getY());
        } else {
            // 其他情况下全部消费
            consumedY = dy;
        }

        // 对父View进行移动
        setY(getY() + consumedY);

        // 将父View消费掉的放入consumed数组中
        consumed[1] = consumedY;

        Log.d(TAG, String.format("onNestedPreScroll, dx = %d, dy = %d, consumed = %s", dx, dy, Arrays.toString(consumed)));
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        Log.d(TAG, String.format("onNestedFling, velocityX = %f, velocityY = %f, consumed = %b", velocityX, velocityY, consumed));
        return true;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        Log.d(TAG, String.format("onNestedPreFling, velocityX = %f, velocityY = %f", velocityX, velocityY));
        return true;
    }

    @Override
    public int getNestedScrollAxes() {
        Log.d(TAG, "getNestedScrollAxes");
        return parentHelper.getNestedScrollAxes();
    }
}

其实也很清晰,接口NestedScrollingParent部分委托给NestedScrollingParentHelper实现,在本例中,我们重点关注onNestedPreScroll这个方法。这个方法就是在子View中调用dispatchNestedPreScroll之后被调用,除了参数offsetInWindow由Helper类控制,其他的参数都是一样的。

父View获取到子View给的dy之后,看要消费多少,把消费的量设置到consumed数组中即可,很简单。


至此这个小例子就写完了,希望能让大家有所启发。

你可能感兴趣的:(android)