NestedScrolling详解

来源:互联网 发布:数据汇集平台 编辑:程序博客网 时间:2024/06/05 12:02

简介

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

基本类和方法

这里需要用到两个接口

NestedScrollingChildNestedScrollingParent

和两个辅助类

NestedScrollingChildHelperNestedScrollingParentHelper

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来实现,其实实现代码是一样的。

原创粉丝点击