Android嵌套滑动机制

一、什么是嵌套滑动机制

我们先来看一下动图,直观的感受下什么是嵌套滚动?

嵌套就说明是一层套着一层,存在两个滑动行为。上图中,当我们滑动下面红色的UI控件时,先滑动的却是其父容器,当父容器滑动到一定程度后,下面红色的UI控件才开始滑动。那么嵌套滑动机制比ViewGroup的事件分发机制有什么优越之处呢?

假设我们按照传统的事件分发去理解,首先我们滑动的是下面的红色的UI控件,而移动却是其父容器,所以按照传统的方式,肯定是父容器拦截了内部的红色的UI控件的事件。但是,当父容器滑动到一定程度时,红色的UI控件又开始滑动了,中间整个过程是没有间断的。从正常的事件分发(不手动调用分发事件,不手动去发出事件)角度去做是不可能的,因为当父容拦截之后,是没有办法再把事件交给红色的UI控件的。事件分发,只要拦截了,当前手势接下来的事件都会交给父容器(拦截者)来处理。

 因此按照事件分发机制是很难达到上面的效果的,要实现上面的效果我们就要使用嵌套滑动机制。

二、嵌套滑动相关类

1、NestedScrollingParent

父容器要实现NestedScrollingParent接口。

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

当子视图调用 startNestedScroll(View, int) 后调用该方法。实现这个方法来声明支持嵌套滚动,如果返回 true,那么这个视图将要配合子视图嵌套滚动。当嵌套滚动结束时会调用到 onStopNestedScroll(View)。nestedScrollAxes表示滑动的方向,水平或者竖直,通过该参数选择仅支持水平滑动或者竖直滑动。

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

如果 onStartNestedScroll 返回 true ,然后执行该方法,这个方法里可以做一些初始化。

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

子视图开始滚动前会调用这个方法。这时候父布局(也就是当前的 NestedScrollingParent 的实现类)可以通过这个方法来配合子视图同时处理滚动事件。

target 滚动的子视图

dx 绝对值为手指在x方向滚动的距离,dx<0 表示手指在屏幕向右滚动

dy 绝对值为手指在y方向滚动的距离,dy<0 表示手指在屏幕向下滚动

consumed 一个数组,值用来表示父布局消耗了多少距离,未消耗前为[0,0], 如果父布局想处理滚动事件,就可以在这个方法的实现中为consumed[0],consumed[1]赋值。分别表示x和y方向消耗的距离。如父布局想在竖直方向(y)完全拦截子视图,那么让 consumed[1] = dy,就把手指产生的触摸事件给拦截了,子视图便响应不到触摸事件了 。

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

这个方法表示子视图正在滚动,并且把滚动距离回调用到该方法,前提是 onStartNestedScroll 返回了 true。

5)、public void onStopNestedScroll(View target);

响应嵌套滚动结束。当一个嵌套滚动结束后(如MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL)会调用该方法,在这里可有做一些收尾工作,比如变量重置

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

手指在屏幕快速滑触发Fling前回调,如果前面 onNestedPreScroll 中父布局消耗了事件,那么这个也会被触发,返回true表示父布局完全处理 fling 事件

 target 滚动的子视图

 velocityX x方向的速度(px/s)

 velocityY y方向的速度

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

子视图fling 时回调,父布局可以选择监听子视图的 fling。true 表示父布局处理 fling,false表示父布局监听子视图的fling

8)、public int getNestedScrollAxes();

返回当前 NestedScrollingParent 的滚动方向。

 2、NestedScrollingChild

子控件要实现NestedScrollingChild接口。

1)、public void setNestedScrollingEnabled(boolean enabled);

设置嵌套滑动是否能用

2)、public boolean isNestedScrollingEnabled();

判断嵌套滑动是否可用

3)、public boolean startNestedScroll(int axes);

开始嵌套滑动, axes 表示方向轴,有横向和竖向

4)、public void stopNestedScroll();

停止嵌套滑动

5)、public boolean hasNestedScrollingParent();

判断是否有父View 支持嵌套滑动

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

在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离

dx x轴上滑动的距离

dy y轴上滑动的距离

consumed 父view消费掉的scroll长度  

offsetInWindow 子View的窗体偏移量

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

子view处理scroll后调用  

dxConsumed x轴上被消费的距离(横向)

dyConsumed y轴上被消费的距离(竖向)

dxUnconsumed x轴上未被消费的距离 dyUnconsumed y轴上未被消费的距离 * @param offsetInWindow 子View的窗体偏移量

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

滑行时调用

velocityX x 轴上的滑动速率

velocityY y 轴上的滑动速率

consumed 是否被消费

9)、public boolean dispatchNestedPreFling(float velocityX, float velocityY);

进行滑行前调用

velocityX x 轴上的滑动速率

velocityY y 轴上的滑动速率

3、NestedScrollingHelper

上面NestedScrollingChild中的大部分方法都可以使用NestedScrollingHelper来实现。具体看后面的例子

三、实现

1、实现NestedScrollingPrent

上面介绍NestedScrollingPrent有很多方法,其实实现3个就可以了:

1)、onStartNestedScroll该方法,一定要按照自己的需求返回true,该方法决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数。假设你只涉及到纵向滑动,这里可以根据nestedScrollAxes这个参数,进行纵向判断。

2)、onNestedPreScroll该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2。

3)、onNestedFling你可以捕获对内部View的fling事件,如果return true则表示拦截掉内部View的事件。

具体代码如下:

public class NestedScrollParent extends LinearLayout implements NestedScrollingParent {
    private static final String TAG = NestedScrollingParent.class.getSimpleName();
    private View head;
    private int headHeight;
    private Scroller scroller;

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

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

    public NestedScrollParent(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        scroller = new Scroller(getContext());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        head = getChildAt(0);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        headHeight = head.getMeasuredHeight();
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {

    }

    @Override
    public void onStopNestedScroll(@NonNull View target) {

    }

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

    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
        int scrollY = getScrollY();

        boolean isHiddeTop = dy > 0 && scrollY < headHeight;
        boolean isShowTop = dy < 0 && getScrollY() > 0 && !target.canScrollVertically(-1);

        if (isHiddeTop || isShowTop) {
            scrollTo(0, dy + scrollY);
            consumed[1] = dy;
        }
    }

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

    @Override
    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
        if (getScrollY() >= headHeight) {
            return false;
        }
        fling((int) velocityY);
        return true;
    }

    @Override
    public int getNestedScrollAxes() {
        return 0;
    }

    public void scrollTo(int x, int y) {
        if (y > headHeight) {
            y = headHeight;
        } else if (y < 0) {
            y = 0;
        }

        if (y != getScrollY()) {
            super.scrollTo(x, y);
        }
    }

    public void fling(int velocityY) {
        scroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, headHeight);
        invalidate();
    }
}

2、实现NestedScrollingChild

NestedScrollingChild的具体实现基本都交给NestedScrollingChildHelper实现了

public class NestedScrollChild extends AppCompatTextView implements NestedScrollingChild {
    private static final String TAG = NestedScrollChild.class.getSimpleName();
    private NestedScrollingChildHelper nestedHelper;

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

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

    public NestedScrollChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

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

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

    @Override
    public boolean isNestedScrollingEnabled() {
        return true;
    }

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

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

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

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
        return nestedHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
        return nestedHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

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

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

    int mLastTouchX;
    int mLastTouchY;
    int[] mScrollConsumed = new int[2];
    int[] mScrollOffset = new int[2];

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchY = (int) (e.getRawY() + 0.5f);
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            case MotionEvent.ACTION_MOVE:
                int x = (int) (e.getRawX() + 0.5f);
                int y = (int) (e.getRawY() + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
                Log.i("tag", "child:dy:" + dy + ",mLastTouchY:" + mLastTouchY + ",y;" + y);
                mLastTouchY = y;
                mLastTouchX = x;

                // 分发滑动,如果父类一点儿也没有消费会返回false
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    // 减去父类消耗的
                    dy -= mScrollConsumed[1];
                    if (dy == 0) {
                        return true;
                    }
                }

                int scrollY = getScrollY();
                Log.i("tag", "scrollY:" + scrollY);
                if (scrollY >= 0) {
                    Log.i("tag", "scrollBy:" + scrollY);
                    scrollTo(0, dy + getScrollY());
                }
                break;
        }

        return true;
    }

    @Override
    public void scrollTo(int x, int y) {
        if (y > getMeasuredHeight()) {
            y = getMeasuredHeight();
        } else if (y < 0) {
            y = 0;
        }

        super.scrollTo(x, y);
    }
}

重点还是在onTouchEvent中的事件分发。ACTION_DOWN时要调用startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL),然后会触发NestedScrollingParent的onStartNestedScroll。  ACTION_MOVE时要先调用dispatchNestedPreScroll,然后会触发NestedScrollingParent的onNestedPreScroll,通知父容器判断是否消耗滑动,然后返回消耗掉得距离。子View再滑动剩下的距离,子View滑动后还有剩余时调用dispatchNestedScroll,然后会触发NestedScrollingParent的onNestedScroll继续消费剩余距离。

四、流程

大致执行流程如下:

Android嵌套滑动机制_第1张图片

1)、子view 需要滑动的时候例如 ACTION_DOWN 的时候就要调用 startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL) 方法来告诉父 view 自己要开始滑动了(实质上是寻找能够配合 child 进行嵌套滚动的 parent)。

2)、父 view 会收到 onStartNestedScroll 回调从而决定是不是要配合子view做出响应。如果需要配合,此方法会返回 true。继而 onStartNestedScroll()回调会被调用。

3)、在滑动事件产生但是子 view 还没处理前可以调用 dispatchNestedPreScroll(0,dy,consumed,offsetInWindow) 这个方法把事件传给父 view 这样父 view 就能在onNestedPreScroll 方法里面收到子 view 的滑动信息,然后做出相应的处理把处理完后的结果通过 consumed 传给子 view。

4)、dispatchNestedPreScroll()之后,child可以进行自己的滚动操作。

5)、如果父 view 需要在子 view 滑动后处理相关事件的话,可以在子 view 的事件处理完成之后调用 子 view 的dispatchNestedScroll, 然后父 view 会在 onNestedScroll 收到回调。

6)、最后,滑动结束,调用 onStopNestedScroll() 表示本次处理结束。

五、注意

1、嵌套滑动机制并没有改变原有的事件分发机制,嵌套滑动机制是在原有的事件分发机制上实现的。

2、自定义NestedScrollParent在onNestedPreScroll()方法中判断是否处理滑动,如果需要处理就在onNestedPreScroll()滑动即可。

你可能感兴趣的:(Android)