Android|嵌套滑动机制

266 阅读6分钟

导语

在传统的触摸事件分发中,如果不手动调用分发事件或者去发出事件,外部View最先拿到触摸事件,一旦它被外部View拦截消费了,内部View无法接收到触摸事件,同理,内部View消费了触摸事件,外部View也没有机会响应触摸事件(对传统事件分发不了解的,可以去看上一章节的解析 juejin.cn/post/741968…

在此场景下,当多个滑动控件嵌套时,想要做滑动联动效果,就会比较复杂。由此,Android推出了嵌套滑动机制(NestedScrolling),简单来讲,可以让我们在不了解事件分发机制的细节的情况下,优雅的处理复杂的嵌套滑动场景。

原理

将嵌套起来的布局分为父View与子View,子View 在处理滑动事件时建立了询问机制:

  • 滑动前询问父View是否需要优先处理
  • 子View处理
  • 滑动后告知父View自己处理情况,再次询问父View是否需要处理剩余

核心的流程图如下

168d85d18cdea467~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.png

接口解析 以RecycleView为子View,NestedScrollView为父View为例

public class LogNestRecycleView extends LogRecyclerView{
    public LogNestRecycleView(@NonNull Context context) {
        super(context);
    }

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

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

    //嵌套滑动机制 子view

    @Override
    public boolean startNestedScroll(int axes, int type) {
        Log.e("whqq", "子view--startNestedScroll" + "开始嵌套滑动,通过父view.onStartNestedScroll询问父view后续是否要参与滑动的处理");
        return super.startNestedScroll(axes, type);
    }

    /**
     * 在滑动之前,将滑动值分发给NestedScrollingParent
     * @param dx 水平方向消费的距离
     * @param dy 垂直方向消费的距离
     * @param consumed 输出坐标数组,consumed[0]为NestedScrollingParent消耗的水平距离、
     * consumed[1]为NestedScrollingParent消耗的垂直距离,此参数可空。
     * @param offsetInWindow 同上dispatchNestedScroll
     * @return 返回NestedScrollingParent是否消费部分或全部滑动值
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
        String log = "子view开始滑动前,通过父view.onNestedPreScroll 告知即将滑动的距离dy " + dy +"询问父view是否需要处理";
        Log.e("whqq", "子view--dispatchNestedPreScroll " + log);
        boolean result = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
        return result;
    }

    /**
     * recycle view有一个dispatchNestedScroll 被内部定义为final了,所以打印日志时看不见
     * 滑动完成后,将已经消费、剩余的滑动值分发给NestedScrollingParent
     * @param dxConsumed 水平方向消费的距离
     * @param dyConsumed 垂直方向消费的距离
     * @param dxUnconsumed 水平方向剩余的距离
     * @param dyUnconsumed 垂直方向剩余的距离
     * @param offsetInWindow 含有View从此方法调用之前到调用完成后的屏幕坐标偏移量,
     * 可以使用这个偏移量来调整预期的输入坐标(即上面4个消费、剩余的距离)跟踪,此参数可空。
     * @return 返回该事件是否被成功分发
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) {
        String log = "子view滑动完成后,通过父view.onNestedScroll 告知子view 以消费距离dxConsumed" + dyUnconsumed + "未消费距离dyUnconsumed " + dyUnconsumed +"询问父view是否需要处理";
        Log.e("whqq", "子view--dispatchNestedScroll" + log);
        return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
    }

}

public class LogNestedScrollView extends NestedScrollView {
    public LogNestedScrollView(@NonNull Context context) {
        super(context);
    }

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

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


    /**
     * 当NestedScrollingChild调用方法startNestedScroll()时,会调用该方法。主要就是通过返
     * 回值告诉系统是否需要对后续的滚动进行处理
     * child:该ViewParent的包含 NestedScrollingChild 的直接子 View,如果只有一层嵌套,和
     * target是同一个View
     * target:本次嵌套滚动的NestedScrollingChild
     * axes:滚动方向
     *
     * @return true:表示我需要进行处理,后续的滚动会触发相应的回到
     * false: 我不需要处理,后面也就不会进行相应的回调了
     */
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        boolean result = super.onStartNestedScroll(child, target, axes, type);
        String log = result ? "我要参与处理" : "我不需要参与处理";
        Log.e("whqq", "父亲 onStartNestedScroll " + log);
        return result;
    }

    /**
     * 如果onStartNestedScroll()方法返回的是true的话,那么紧接着就会调用该方法.它是让嵌套滚
     * 动在开始滚动之前,
     * 让布局容器(viewGroup)或者它的父类执行一些配置的初始化的
     */
    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
        Log.e("whqq", "父亲 onNestedScrollAccepted ");
        super.onNestedScrollAccepted(child, target, axes, type);
    }


    /**
     * 当子view调用dispatchNestedPreScroll()方法是,会调用该方法。也就是在
     * NestedScrollingChild在处理滑动之前,
     * 会先将机会给Parent处理。如果Parent想先消费部分滚动距离,将消费的距离放入consumed
     * dx:水平滑动距离
     * dy:处置滑动距离
     * consumed:表示Parent要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y
     * 方向上消费的距离.
     */

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        String log = "即将滑动的距离dy: "+dy+" 父亲需要消费的距离: "+consumed[1];
        Log.e("whqq", "父亲 onNestedPreScroll "+log);
        super.onNestedPreScroll(target, dx, dy, consumed, type);
    }


    /**
     * 当子view调用dispatchNestedScroll()方法时,会调用该方法。也就是开始分发处理嵌套滑动了
     * dxConsumed:已经被target消费掉的水平方向的滑动距离
     * dyConsumed:已经被target消费掉的垂直方向的滑动距离
     * dxUnconsumed:未被tagert消费掉的水平方向的滑动距离
     * dyUnconsumed:未被tagert消费掉的垂直方向的滑动距离
     */
    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
                               @NonNull int[] consumed) {
        String log = "已被子view消费距离:"+dyConsumed +" 未被子view消费距离:"+dyUnconsumed;
        Log.e("whqq", "父亲 onNestedScroll"+log);
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
    }

}

日志打印,方便理解

image.png

应用场景

从上述各个接口的含义可知,日常开发处理的场景主要在子view与父view相互询问是否要消费滚动参数的位置,去处理自定义的功能要求

**onNestedPreScroll:**即将滑动的距离,在子view滑动前询问父view是否需要消费,父view可在此处处理滑动事件

**onNestedScroll:**子view消费完滑动事件后,剩余未消费的事件通知父view,父view可在此处理消费剩余滑动事件的逻辑

举例

1、NestedScrollView嵌套RecycleView,手指在RecycleView中滑动,当RecycleView不可滑动时,NestedScrollView 不会跟随滚动


public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
                           @NonNull int[] consumed) {
     //子view未能完全消费场景,不通知父亲view,父view收到的未消费滚动事件永远为0,因此不会滚动      
    int adapterY = 0;
    super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, adapterY, type, consumed);
}

2、NestedScrollView嵌套RecycleView,手指在RecycleView中滑动,fling操作仅在内部传递,不传递给父view

public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
                           @NonNull int[] consumed) {
    int adapterY = dyUnconsumed;
    //子view正在消费,但未能完全消费场景,不通知父亲view
    if (dyConsumed != 0 && dyUnconsumed != 0) {
        adapterY = 0;
    }
    super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, adapterY, type, consumed);
}

public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
    float adapterY = velocityY;
    adapterY = 0;
    return super.onNestedFling(target, velocityX, adapterY, consumed);
}

大佬们的文章

juejin.cn/post/684490…

juejin.cn/post/684490…