Android 嵌套滑动解决

NestedScrollingParent与NestedScrollingChild

1、 嵌套滑动的解决方案先看注释了解方法

这时Google官方给的处理方案,在Androidx或者support包中

public interface NestedScrollingChild {
  
    //设置是否允许嵌套滑动,允许的话设为true
    void setNestedScrollingEnabled(boolean enabled);
 
    //是否允许嵌套滑动
    boolean isNestedScrollingEnabled();

    //开始嵌套滑动 axes:滑动方向,一般用于通知父控件我要看是滑动了
    boolean startNestedScroll(@ScrollAxis int axes);
 
    //停止嵌套滑动,一般也用于通知父控件
    void stopNestedScroll();

    // 判断父控件NestedScrollingParent 的onStartNestedScroll方法是否返回true,
    //只有true,才能继续一系列的嵌套滑动
    boolean hasNestedScrollingParent();

    /**
     * 子控件消费了一部分滑动之后通知父控件滑动
     * @param dxConsumed    子控件X方向已消耗长度
     * @param dyConsumed    子控件Y方向已消耗长度
     * @param dxUnconsumed  子控件X方向未消耗长度
     * @param dyUnconsumed  子控件Y方向未消耗长度
     * @param offsetInWindow  控件在window上的偏移
     * @return 数据是否可以分发
     */
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);

    /**
     * 子控件滑动之前通知父控件滑动
     * @param dx    x方向上触摸滑动距离
     * @param dy    y方向上触摸滑动距离
     * @param consumed  父控件滑动后未消耗的距离,是父控件返回的值,不是子控件的值
     *                   consumed[0]-X  consumed[1]为Y 
     * @param offsetInWindow  控件在window上的偏移
     * @return  数据是否可以分发
     */
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow);

    //子控件消耗Fling滑动后通知父控件,velocityX每秒滑动速度,
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    //子控件消耗Fling滑动之前通知父控件fling滑动
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
public interface NestedScrollingParent {
    //子控件开始嵌套滑动通知了父控件,这个方法接收调用,父控件决定是否嵌套滑动 @return 是否嵌套滑动
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
 
    //子控件开始嵌套滑动通知了父控件,这个方法也会接收调用,
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

    //子控件停止嵌套滑动通知父控件
    void onStopNestedScroll(@NonNull View target);

    //子控件调用 dispatchNestedScroll 父控件这个方法接收调用
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    //子控件调用 dispatchNestedPreScroll 父控件这个方法接收调用 consumed需要父控件赋值
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
 
     //子控件调用 dispatchNestedFling 父控件这个方法接收调用,惯性滑动
    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);

    //子控件调用 dispatchNestedPreFling 父控件这个方法接收调用,惯性滑动
    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);

    //获取当前父控件的滑动方向
    int getNestedScrollAxes();
}

了解Android事件分发的可以思考一下,事件分发是父控件分发给子控件,父控件也是dispath方法分发,子控件on方法接收,这里其实就是反过来,子控件dispath分发,父控件接收处理,处理结果有返回给子控件。

2、嵌套滑动调用流程

子view 父View
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll

子控件开始滑动调用startNestedScroll通知父控件我要看是滑动了,父控件就会被调用onStartNestedScroll、onNestedScrollAccepted,并返回给子控件我要不要滑动,如果父控件不滑动到此结束,子控件自己滑动就行了,
父控件需要滑动,则子控件开始决定怎样配合滑动,

  1. 子控件先滑动父控件后滑动,子控件滑动后,调用dispatchNestedScroll,父控件onNestedScroll被调用,如果有父控件没消费的一部分滑动由参数dxUnconsumed返回给子控件继续滑动,
  2. 父控件先滑动子控件后滑动,子控件先调用dispatchNestedPreScroll,父控件则onNestedPreScroll被调用,父控件滑动后将没有消耗的滑动赋值给consumed,交给子控件,子控件开始滑动,
    3:dispatchNestedFling与dispatchNestedPreFling一样逻辑

3、示例解析

示例源码

3.1 自定义NestedScrollChildLayout和NestedScrollParentLayout子父控件

只要实现NestedScrollingChild 和 NestedScrollingParent接口即可,实现所有方法。

3.2 NestedScrollingChildHelper与NestedScrollingParentHelper

通过名字可以看出这是子控件和父控件嵌套滑动各个控件的帮助类,他们的方法几乎都对应着两个嵌套滑动接口。在androidx或support包中。

  1. NestedScrollingChildHelper: 子控件调用,寻找父控件并调用父控件中的NestedScrollingParent方法。
  2. NestedScrollingParentHelper:父控件调用,寻找子控件调用子控件的NestedScrollingChild 方法。

3.3 子控件处理触摸滑动事件实现嵌套滑动

触摸滑动处理方法 handleMoveY(int distanceY), 这里只处理上下滑动

    private void handleMoveY(int distanceY){
        //父控件是否允许滑动
        if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)) {
            if (distanceY > 0) {
                //向上滑, 父控件先滑动子控件再滑动
                if (dispatchNestedPreScroll(0,   distanceY, consumed, offset)) {
                    //consumed为父控件消耗的距离, 未消耗的子控件继续滑动
                    int unConsumed =   distanceY - consumed[1];
                    if (unConsumed != 0) {
                        scrollBy(0, unConsumed);
                    }
                } else {
                    scrollBy(0,   distanceY);
                }
            } else {
                //向下滑, 子控件先滑动父控件后滑动
                if (getScrollY() >= 0) {
                    if (getScrollY() == 0) {
                        //子控件已不再需要滑动,父控件滑动
                        dispatchNestedScroll(0, 0, 0, distanceY, offset);
                        return ;
                    }
                    //子控件可以消耗所有滑动,先滑动自己
                    if (getScrollY() +   distanceY >= 0) {
                        scrollBy(0,   distanceY);
                    } else if (getScrollY() != 0) {
                        //子控件滑动一部分,剩余给父控件滑动
                        int consume = getScrollY();
                        int unConsumed = (int) distanceY + consume;
                        scrollTo(0, -consume);  //先自己滑动
                        dispatchNestedScroll(0, consume, 0, unConsumed, offset);
                    } else {
                        dispatchNestedScroll(0, 0, 0,   distanceY, offset);
                    }
                } else {
                    scrollTo(0, 0);
                }
            }
        }
    }

由上面注释可以看出,嵌套滑动完全按照 2、嵌套滑动调用流程来完成,

  1. 向上滑,父控件先消耗一定的滑动距离,子控件滑动剩余距离。父控件则隐藏第一个控件。
  2. 向下滑,子控件先滑动到顶部,之后将剩余的滑动距离或后续的滑动分发给父控件。父控件显示出第一个控件。

3.4 父控件处理子控件分发的滑动

父控件需要的滑动是隐藏或显示第一个控件,则父控件可滑动的距离就是第一个控件的高度

    //向下滑动,子控件已滑动顶部,分发剩余滑动与后续的滑动,父控件开始滑入第一个控件。
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        Log.i(TAG, "onNestedScroll: dyConsumed=" + dxConsumed + "  dyUnconsumed=" + dyUnconsumed);
        //父控件后滑动为显示上部控件
        if (getScrollY() > 0 && dyUnconsumed < 0) {
            if (getScrollY() + dyUnconsumed >0) {
                scrollBy(0, dyUnconsumed);
            } else {
                scrollTo(0, 0);
            }
        }
    }

    //向上滑动,父控件先滑动,将已消耗的滑动距离传给子控件,子控件继续滑动,实现父控件第一个控件滑出屏幕。
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(target, dx, dy, consumed);
        Log.i(TAG, "onNestedPreScroll: dx=" + dx + " dy=" + dy);
        View view = getChildAt(0);
        //父控件先滑动为隐藏上部控件,
        if (getScrollY() < view.getHeight() && dy > 0) {
            if (getScrollY() + dy <= view.getHeight()) {
                scrollBy(0, dy);
                consumed[1] = dy;
            } else {
                consumed[1] = view.getHeight() - getScrollY();
                scrollTo(0, view.getHeight());
            }
        }
    }

4. 接口系统化

在Android api 21之后,嵌套滑动的的接口已经系统化,Android原生系统view与ViewGroup就自带了嵌套滑动的所有接口方法,

推荐阅读更多精彩内容