NestedScrolling详解

简介

假设我们需要一个这样的效果,拖动子View的时候需要parent先滑动,等parent滑倒顶端的时候再让子View滑动。Android事件分发机制在parent处理事件的时候,没法再次把事件传递给子View(除非再来一个Down,开启一个新的事件序列),所以就需要用到NestedScrolling,也就是嵌套滑动机制。今天我们来实现如下效果
NestedScrolling详解_第1张图片
蓝色部分是子View,粉色是Parent,在向上滑动时,保证Parent首先滑动到顶端,向下滑动时保证子View首先滑倒底部。

基本类和方法

这里需要用到两个接口

NestedScrollingChild
NestedScrollingParent

和两个辅助类

NestedScrollingChildHelper
NestedScrollingParentHelper

NestedScrollingChild

子View实现这个接口


    public void setNestedScrollingEnabled(boolean enabled);

    public boolean isNestedScrollingEnabled();

    public boolean startNestedScroll(int axes);

    public void stopNestedScroll();

    public boolean hasNestedScrollingParent();

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
  • void setNestedScrollingEnabled(boolean enabled):允许嵌套滑动
  • boolean startNestedScroll(int axes):一般在ACTION_DOWN的事件里调用,表示要开始滑动,axes代表方向,有SCROLL_AXIS_VERTICAL、SCROLL_AXIS_HORIZONTAL两种
  • boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow):一般在ACTION_MOVE种调用,dx、dy是将要滑动的量,然后分发给Parent让他消耗,consumed是一个二维数组,分别存储Parent消耗的x、y方向上的量,如果无消耗那么返回false。

NestedScrollingParent

Parent实现这个接口

    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    public void onStopNestedScroll(View target);

    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    public int getNestedScrollAxes();
  • void onNestedPreScroll(View target, int dx, int dy, int[] consumed):子View调用dispatchNestedPreScroll的时候此方法会被回调,通过判断dx、dy来计算消耗,返回消耗值。

然而真正的逻辑实现都由Helper类帮我们实现了,我们只需要调用helper类的对应方法即可,接下来开始写代码。

ChildView代码


/**
 * @author wulinpeng
 * @datetime: 17/6/17 下午10:34
 * @description:
 */
public class ChildView extends View implements NestedScrollingChild {

    private NestedScrollingChildHelper helper;

    private float lastY = 0;

    private int[] consume = new int[2];

    private int[] offset = new int[2];

    public ChildView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastY = event.getY();
                // 开始垂直的滑动
                helper.startNestedScroll(SCROLL_AXIS_VERTICAL);
                break;
            case MotionEvent.ACTION_MOVE:
                // 获得滑动量
                int dy = (int) (event.getY() - lastY);
                if (dy < 0) {
                    // 向上滑动的逻辑,保证parent消耗,才到自己
                    if (!helper.dispatchNestedPreScroll(0, (int) dy, consume, offset)) {
                        // 运行到这说明parent不消耗了,parent已经到达顶部,这时候自身滑动
                        // 因为向上滑动dy < 0,所以*-1方便比较
                        int space = (int) getY() * -1;
                        int consumeY = Math.max(space, dy);
                        setY(getY() + consumeY);
                    }
                } else {
                    // 向下滑动的逻辑,保证自己消耗,才到parent
                    int space = (int) (((ParentView) getParent()).getHeight() - getY() - getHeight());
                    int consumeY = Math.min(space, dy);
                    dy -= consumeY;
                    setY(getY() + consumeY);
                    // 自己消耗完后,然后传给Parent剩下的dy-consumeY
                    helper.dispatchNestedPreScroll(0, (int) dy, consume, offset);
                }
                break;
            case MotionEvent.ACTION_UP:

                break;
        }
        return true;
    }

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

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

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

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

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

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

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

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

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

注释比较清楚了,主要就是方向不同逻辑不同,向上的时候先分发给Parent,如果Parent不消耗了(返回false,也就是说到达顶部了),那么自己消耗dy(向上滑动,注意越界情况);向下的时候,首先自己向下滑动(自己消耗dy),然后给Parent分发消耗后的dy。

ParentView代码

/**
 * @author wulinpeng
 * @datetime: 17/6/17 下午10:37
 * @description:
 */
public class ParentView extends FrameLayout implements NestedScrollingParent {

    private NestedScrollingParentHelper helper;

    public ParentView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        helper = new NestedScrollingParentHelper(this);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        FrameLayout parent = (FrameLayout) getParent();
        if (dy > 0) {
            // 向下滑动
            int space = (int) (parent.getHeight() - getY() - getHeight());
            int consumeY = Math.min(dy, space);
            consumed[1] = consumeY;
            setY(getY() + consumeY);
        } else {
            // 向上滑动
            int space = (int) (getY() * -1);
            int consumeY = Math.max(dy, space);
            consumed[1] = consumeY;
            setY(getY() + consumeY);
        }
    }

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

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

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

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

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

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    }
}

比较简单,主要是注意越界的情况,接下来只要在布局文件里将ChildView设置为ParentView的child就可以了。

源码解析

但是这两者到底是怎么样联系起来的呢?我们看看Helper类的源码

    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

startNestedScroll是最开始要调用的,作用就是把这个Child和Paren联系起来,内部首先寻找可用的Parent,然后回调Parent的onStartNestedScroll方法,如果返回true,那么就给内部的mNestedScrollingParent赋值同时回调Parent的onNestedScrollAccepted方法,否则mNestedScrollingParent还是null。如果已经有了Parent那么直接返回true,可以知道这个方法调用一次就可以了,只要和Parent联系起来就ok。

    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;
    }

这个方法首先判断isNestedScrollingEnabled和mNestedScrollingParent,如果mNestedScrollingParent==null也就是Parent在onStartNestedScroll返回了false,那么就不会收到这个分发。方法内部回调了Parent的onNestedPreScroll方法,然后判断consume的两个值,如果都是0,那么说明Parent没有消耗,就返回false表示Parent不消耗。

总结

其实就是NestedScrollingChild发出各种事件,比如最开始的startNestedScroll来寻找可用的Parent同时回调Parent的方法,dispatchNestedPreScroll分发偏移量给Parent让它先消耗,而NestedScrollParent只是被动接受各种回调作出处理,比如在onStartNestedScroll返回boolean表示是否接受嵌套滑动,在onNestedPreScroll消耗滑动偏移量。其实高版本的View默认实现了这些方法,但是为了兼容低版本,我们是用Helper来实现,其实实现代码是一样的。

你可能感兴趣的:(Android,Android自定义控件)