事件分发和NestedScrolling(二)

前言

NestedScrolling是Android 5.0上一套全新的嵌套滑动机制,5.0及以上版本的所有View已经默认
支持了这套机制,老的版本可以通过Support包进行向前兼容。

它涉及的接口和类有下面四个:

  • 主要接口:NestedScrollingChild、NestedScrollingParent

  • 帮助类:NestedScrollingChildHelper、NestedScrollingParentHelper

NestedScrolling机制不同于常见的事件分发机制,它不是全局性的,它只在特定场景下对父View和子View的事件滚动进行分发处理,它是为业务需要而生的一种机制,所以要根据实际情况来熟悉和理解这种机制,由于方法很多,参数也多,使用起来也不是那么明朗,所以这里先举两个例子来引出我们的NestedScrolling

使用场景

为了帮助理解嵌套滚动,我用上一篇文章的事件分发来对比描述NestedScrolling的作用。没有看过或者不怎么了解事件冲突的朋友,请先看上一篇文章 《事件分发和NestedScrolling(一)》

场景一

触摸子View并在Y轴上产生了100px的滚动事件,此刻父View不需要事件,而子View只需要60px的滚动,如果再滚动时,交给父View。

如果用外部拦截的方式,程序大致流程如下:

//外部拦截View(onInterceptTouchEvent):
此刻我不需要拦截事件

子View(onTouchEvent):
消耗事件,但是我只能消耗60px,还剩下40px。。
唉,直接抛弃吧~

**此时又发生了同方向上的ACTION_MOVE**

父View(onInterceptTouchEvent):
现在我需要拦截事件,不然子View都滚出屏幕了

**父View决定拦截事件,子View收到ACTION_CANCEL的事件,同时mFirstTouchTarget链表会
被重置,此后所有的触摸事件即交由父View处理,直到下一次ACTION_DOWN发生**

如果用内部拦截的方式,程序大致流程如下:

//内部拦截View(dispatchTouchEvent):
ACTION_DOWN发生时,通知父View:请先别拦截我的事件,等我通知您时您再拦截

父View(onInterceptTouchEvent):
好的,只要子view不消耗的事件,我都要

子View(onTouchEvent):
父View已经答应不拦截了,我现在消耗事件,但是只能消耗60px,还剩下40px。。
唉,直接抛弃吧~

**此时又发生了同方向上的ACTION_MOVE**

子View(dispatchTouchEvent):
我现在不需要消耗事件了,现在通知父View:您现在可以拦截我的事件了

父View(onInterceptTouchEvent):
子View允许我拦截了,我现在拦截事件吧

父View(onTouchEvent):
消费事件

**父View决定拦截事件,子View收到ACTION_CANCEL的事件,同时mFirstTouchTarget链表会
被重置,此后所有的触摸事件即交由父View处理,直到下一次ACTION_DOWN发生**

如果用NestedScrolling机制来实现,应该怎么做呢?

//NestedScrolling

**开始**

子View(onTouchEvent):
我这里有100px的滚动事件,您要消费吗

**子View调用dispatchNestedPreScroll(100)和父View沟通,父View的onNestedPreScroll收到回调**

父View(onNestedPreScroll):
我暂时不需要,如果你有消费不完的,再告诉我吧

子View(onTouchEvent):
父View一点也没有消耗,那我接着消耗吧~
根据实际情况,我消耗了60px,还剩下40px,现在把消耗不完的滚动传给父View

**子View调用 dispatchNestedScroll(40)和父View沟通,父View的onNestedScroll收到回调**

父View(onNestedScroll):
好,我知道了,剩下的40px我来消耗吧
消耗40px

**结束**

可以看到,在常规的事件分发机制中,有两个明显不完美的地方:

  • 拦截只是一锤子买卖,父View一旦决定拦截,子View将再也没有机会接受处理事件了

  • 手指不停的滑动,两次ACTION_MOVE的距离如果超过了子View实际消耗的距离,那么剩下的距离也无法分割给父View,子View只能抛弃,直到下一次ACTION_MOVE的发生

而在NestedScrolling机制中,子View可以和父View彼此协商,父View可以消耗一部分,然后如果还有剩余就让子View消费,如果子View消费不完了还有剩余,那就又给父View消费,一来一回,和接力赛跑一样,父View和子View彼此协商彼此接力,直到把所有的距离都消费完。

场景二

我们再来看一个场景:触摸子View并在Y轴上产生了100px的滚动事件,若父View需要先拦截30px的滚动,而子View需要在父View
滚动完之后进行60px的滚动,剩下的10px继续交由父View进行滚动。

如果用外部拦截,程序大致流程如下:

//外部拦截View(onInterceptTouchEvent):
我需要要30px,所以我要拦截这次事件

父View(onTouchEvent):
消耗事件,但是还有多的70px没被处理。。
唉,直接抛弃吧~

**父View一旦开始消费触摸事件,后续发生的所有触摸事件都将继续由它处理,直到下一次ACTION_DOWN事件的发生**

**再次ACTION_DOWN**

父View(onInterceptTouchEvent):
此刻我不需要拦截事件

子View(onTouchEvent):
消耗事件,但是我只能消耗60px,还剩下40px。。
唉,直接抛弃吧~

**此时又发生了同方向上的ACTION_MOVE**

父View(onInterceptTouchEvent):
现在我需要拦截事件,不然子View都滚出屏幕了

**父View决定拦截事件,子View收到ACTION_CANCEL的事件,同时mFirstTouchTarget链表会
被重置,此后所有的触摸事件即交由父View处理,直到下一次ACTION_DOWN发生**

对于这个场景,用外部拦截的方法在第一次ACTION_DOWN事件序列中,子View完全没有机会收到事件进行消费滚动,除非手指抬起再按下,父View不需要拦截消费了,直接把事件交给子View,然后子Viw消费不完的再让父View拦截,这就回到场景一了。

所以对于场景二,其实需要两次触摸,一次触摸让父View滚动完毕,第二次触摸完成场景一,而且这两次触摸中间,如果手指不抬起来再按下,会导致滑动控件卡在父View滚动完成和子View即将开始滚动的那个临界点,产生滑不动的情况。相信有过相关经历的同学应该能理解我说的话。

那用内部拦截是什么样呢?

//内部拦截View(dispatchTouchEvent):
ACTION_DOWN发生时,通知父View:请先别拦截我的事件,等我通知您时您再拦截

父View(onInterceptTouchEvent):
好的,只要子view不消耗的事件,我都要

子View(dispatchTouchEvent):
我还没有到需要滚动的时候,父View需要30px的滚动,这次的滚动事件先给他吧
通知父View:您可以拦截我的事件了,您先消耗吧

父View(onTouchEvent):
好的,我消耗事件,但是还有多的70px没被处理。。
唉,直接抛弃吧~

**父View一旦开始消费触摸事件,后续发生的所有触摸事件都将继续由它处理,直到下一次ACTION_DOWN事件的发生**

**再次ACTION_DOWN**

子View(dispatchTouchEvent):
ACTION_DOWN发生时,通知父View:请先别拦截我的事件,等我通知您时您再拦截

父View(onInterceptTouchEvent):
好的,只要子view不消耗的事件,我都要

子View(onTouchEvent):
父View已经答应不拦截了,我现在消耗事件,但是只能消耗60px,还剩下40px。。
唉,直接抛弃吧~

**此时又发生了同方向上的ACTION_MOVE**

子View(dispatchTouchEvent):
我现在不需要消耗事件了,现在通知父View:您现在可以拦截我的事件了

父View(onInterceptTouchEvent):
子View允许我拦截了,我现在拦截事件吧

父View(onTouchEvent):
消费事件

**父View决定拦截事件,子View收到ACTION_CANCEL的事件,同时mFirstTouchTarget链表会
被重置,此后所有的触摸事件即交由父View处理,直到下一次ACTION_DOWN发生**

产生的问题和外部拦截一样,同样需要发生两次ACTION_DOWN才能实现场景二的需求,而且在临界点会卡住导致滑不动。

如果用NestedScrolling机制来实现是什么样呢?

**开始**

子View(onTouchEvent):
我这里有100px的滚动事件,您要消费吗

**子View调用dispatchNestedPreScroll(100)和父View沟通,父View的onNestedPreScroll收到回调**

父View(onNestedPreScroll):
好,我目前先消费30px,剩下的你来消耗,你如果还有消费不完的话,再跟我说下吧

子View(onTouchEvent):
父View消耗了30px,还剩下70px,那我接着消耗吧~
根据实际情况,我消耗了60px,还剩下10px,现在把消耗不完的滚动传给父View

**子View调用 dispatchNestedScroll(10)和父View沟通,父View的onNestedScroll收到回调**

父View(onNestedScroll):
好,我知道了,剩下的10px我来消耗吧
消耗10px

**完**

可以看到,在一次触摸事件中就已经完美的解决了滑动分发的任务,不需要两次ACTION_DOWN,也不会导致卡住,而且极端情况下,如果父View和子View要进行超过两次以上的交互,也可以满足。

”生命周期“

生命周期只是我个人的一种叫法,实际上官方并没有这么个东西,我之所以这么称呼,是因为这套机制初次理解会很麻烦,不仅类多、方法多而且参数也多,但是方法之间又是彼此联系的,所以如果能分门别类进行记忆,对不了解这套机制的同学理解起来应该会有很大的便利。

NestedScrollingChildNestedScrollingParent
初始化 setNestedScrollingEnabled
startNestedScroll
hasNestedScrollingParent
isNestedScrollingEnabled
onStartNestedScroll
onNestedScrollAccepted
Scroll dispatchNestedPreScroll
dispatchNestedScroll
onNestedPreScroll
onNestedScroll
Fling dispatchNestedPreFling
dispatchNestedFling
onNestedPreFling
onNestedFling
结束 stopNestedScroll onStopNestedScroll

可以看到,子View的很多方法在父View中都能找到,基本上是一一对应的,但是子View的方法大多是以dispatch开头,而父View的方法是以on开头,由此可见,在NestedScrolling机制中,子View是主导方。

虽然方法很多,但是这样一分类,是不是清晰很多了呢?

方法介绍

上面用 “ 生命周期 ” 进行分类,我们接下来看看它一般在代码中的使用方式。

NestedScrolling

上图列出来手指从按下到抬起时的整个流程,当然这些都是在子View的onTouchEvent()中完成的,所以父View一定不能拦截子View的事件,否则这套机制就失效了。

除此之外,箭头的左边分别都是NestedScrollingChild中的各种方法,右边则是NestedScrollingParent对应的方法。使用时,一般是子View通过dispatchXXX()来通知父View,然后父View通过onXXX()来进行回应。

方法调用的先后时机也有区别,对应到上图中,图越往下,调用的时机越晚。

下图是NestedScrollingChildNestedScrollingParent中全部的方法,方法旁边的序号表示他们调用的先后顺序。

NestedScrolling

看到这么多的方法和参数,是不是有点头晕想放弃,不慌,其实没有那么复杂。

图中方法旁边的序号分别用了四种不同的颜色,分别代表实现的难易程度。

  • 绿色:是最简单的,系统已经封装好了,我们不需要实现和关心

  • 蓝色:常规实现,直接复制粘贴就可以了,无脑操作

  • 红色:核心实现,也是和我们的业务紧密关联的部分,需要我们手动实现里面的逻辑,比较复杂

  • 粉红色:同样是核心实现,但是不是必须的,比如fling操作,如果项目中不需要fling的逻辑,不实现即可

使用方法

说了这么多,具体怎么用呢?我们先来看看最基本的部分,也是所有使用NestedScrolling机制都需要完成的必须步骤。

在子View中,首先要实现NestedScrollingChild接口,并复写接口中的所有方法:

// 子View
public class ChildView extends FrameLayout implements NestedScrollingChild, INestedView {

    private NestedScrollingChildHelper mChildHelper;
    private int[] mConsume = new int[2], mOffsetInWindow = new int[2];

    public ChildView(@NonNull Context context) {
        this(context, null);
    }

    public ChildView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    public NestedScrollingChildHelper getChildHelper() {
        if (mChildHelper == null) {
            mChildHelper = new NestedScrollingChildHelper(this);
            mChildHelper.setNestedScrollingEnabled(true); // 支持嵌套滚动
        }
        return mChildHelper;
    }

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

    //是否支持嵌套滚动
    @Override
    public boolean isNestedScrollingEnabled() {
        return getChildHelper().isNestedScrollingEnabled();
    }

    // axes 表示滚动方向,水平或垂直
    @Override
    public boolean startNestedScroll(int axes) {
        return getChildHelper().startNestedScroll(axes);
    }

    // 停止嵌套滚动
    @Override
    public void stopNestedScroll() {
        getChildHelper().stopNestedScroll();
    }

    // 当前是否有嵌套滚动的parent,如果有表示正在嵌套滚动
    @Override
    public boolean hasNestedScrollingParent() {
        return getChildHelper().hasNestedScrollingParent();
    }

    /**
    * 子View还有滚动距离没有消费完时,通知父View的方法
    * @param dyConsumed 竖直方向上消耗的距离 
    * @param dyUnconsumed 竖直方向上剩余的距离 
    * @param offsetInWindow 二维数组,存放父View嵌套滚动之后在水平和竖直方向上的偏移量
    */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return getChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    /**
    * 子View还有滚动距离没有消费完时,通知父View的方法
    * @param dy 竖直方向上的滚动距离
    * @param consumed 二维数组,存放父View嵌套滚动之后在水平和竖直方向上消费的距离
    * @param offsetInWindow 二维数组,存放父View嵌套滚动之后在水平和竖直方向上的偏移量
    */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return getChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    /**
    *@param velocityY 竖直方向上的速度
    */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
    }

    /**
    *@param velocityY 竖直方向上的速度
    */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getChildHelper().dispatchNestedPreFling(velocityX, velocityY);
    }
}

然后在父View中,实现NestedScrollingParent接口中的所有方法:

// 父View
public class ParentView extends LinearLayout implements NestedScrollingParent {

    private NestedScrollingParentHelper mParentHelper;

    public ParentView(@NonNull Context context) {
        this(context, null);
    }

    public ParentView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    public NestedScrollingParentHelper getParentHelper() {
        if (mParentHelper == null) {
            mParentHelper = new NestedScrollingParentHelper(this);
        }
        return mParentHelper;
    }

    /*
    * 子View调用startNestedScroll时会被回调,nestedScrollAxes表示滚动方向,分为垂直方向和水平方向
    * @return 返回值表示是否支持该方向的嵌套滚动
    */
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return nestedScrollAxes == SCROLL_AXIS_VERTICAL;
    }

    //接受当前方向的滚动时会回调
    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        getParentHelper().onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    //子View调用stopNestedScroll时会被回调
    @Override
    public void onStopNestedScroll(View target) {
        getParentHelper().onStopNestedScroll(target);
    }

    /**
    *子View调用dispatcPrehNestedPreScroll方法后得到回调
    *
    *@param dy 竖直方向上可以滚动的距离
    *@param consumed 二维数组,滚动完成之后,可以把水平、竖直方向上的滚动距离存放在该数组中,以让子View知晓父View的滚动距离
    */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int deltaY = doNestedPreScroll(dy);
        consumed[1] = deltaY;
    }

    // 滚动
    private int doNestedPreScroll(int dy) {
        int deltaY = 若干像素;
        scrollBy(0, deltaY);
        return deltaY;
    }

    /**
    *子View调用dispatchNestedPreScroll方法后得到回调
    *
    *@param dyConsumed 竖直方向上消费了的距离
    *@param dyUnconsumed 竖直方向上剩余的距离
    */
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        //这里的dy是父View第一次消费完和子View消费完之后剩下没有消费的距离
        int deltaY = doNestedScroll(dyUnconsumed);
    }

    private int doNestedScroll(int dy) {
        int deltaY = dy;
        scrollBy(0, deltaY);
        return deltaY;
    }

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

    /**
    *@param velocityY 竖直方向上的速度
    */
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        doNestedFling((int) velocityY);
        return true;
    }

    public void doNestedFling(int velocityY) {
        //do fling
    }

    @Override
    public int getNestedScrollAxes() {
        return getParentHelper().getNestedScrollAxes();
    }
}

基本实现就是这些了,每次使用NestedScrolling机制都会用到这些,以后写的时候也可以直接把它当成模板复制粘贴过去。

但是核心的部分还没有实现,前面说了,NestedScrolling的整套机制都是在子View的onTouchEvent()中完成的,所以我们只需要在子View的onTouchEvent()中处理完所有的事情就可以了。

用之前场景二的例子来讲:触摸子View并在Y轴上产生了100px的滚动事件,若父View需要先拦截30px的滚动,而子View需要在父View
滚动完之后进行60px的滚动,剩下的10px继续交由父View进行滚动。

这样一个场景,具体到代码中是怎么实现呢?

// 子View:
onTouchEvent(MotionEvent event){
    int action = MotionEventCompat.getActionMask(event);
    switch(action){
       case MotionEvent.ACTION_DOWN:
       mChildHelper.startNestedScroll(SCROLL_AXIS_VERTICAL);
       break;
      case MotionEvent.ACTION_MOVE:
      int dy = 100; //假设总的滚动距离是100px
      mLastY = event.getRawY();
      if (dispatchNestedPreScroll(0, dy, mConsume, mOffsetInWindow)) {  //询问父View是否需要滚动
            dy -= mConsume[1];  //减去父View滚动消耗了的距离
        }
        int deltaY = calculateAndScrollY(dy); //子View计算并进行滚动,假设这里要滚60px
        if (dy != deltaY) {
            dispatchNestedScroll(0, deltaY, 0, dy - deltaY, mOffsetInWindow);  // 减去父View之前滚动的和自己本身滚动的
                                                                               //距离之后,还剩下10px的滚动,自己接着滚
        }
        break;
        case MotionEvent.ACTION_UP:
        stopNestedScroll();
        break;
    }
}

同时,在父View中处理回调,配合子View进行嵌套滚动

// 父View
//初步消费
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    int deltaY = 30;  //假设这里要滚动30px的距离
    scrollBy(0 , 30); //执行滚动
    consumed[1] = 30;
}
//处理自己初步消费和子View消费之后,仍然剩余的距离
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed) {
    scrollBy(0 , dyUnconsumed); //执行滚动
}

源码写的很清楚,不需要讲了吧。

源码走读

为什么子View调用dispatchXXX方法之后,父View会收到onXXX的回调呢?

由于嵌套滚动开始的源头都在子View的onTouchEvent()方法中的ACTION_DOWN开始,调用了mChildHelper.startNestedScroll(SCROLL_AXIS_VERTICAL),所以先从这里开始:

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

第3~6行,如果已经有支持嵌套滚动的parent,则说明已经在嵌套滚动了,直接返回;如果没有,则继续。

第7~21行,如果子View支持嵌套滚动,则循环遍历该View的父View,找到一个可以支持该方向(axes表示的方向)嵌套滚动的父View。

第8行,调用ViewParentCompat.onStartNestedScroll()方法通知父View。

//ViewParentCompat
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
        return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
    }

查看IMPL的值,是根据不同安卓版本做的兼容:

    //ViewParentCompat
    static final ViewParentCompatImpl IMPL;
    static {
        final int version = Build.VERSION.SDK_INT;
        if (version >= 21) {
            IMPL = new ViewParentCompatLollipopImpl();
        } else if (version >= 19) {
            IMPL = new ViewParentCompatKitKatImpl();
        } else if (version >= 14) {
            IMPL = new ViewParentCompatICSImpl();
        } else {
            IMPL = new ViewParentCompatStubImpl();
        }
    }

所以在version > 21的安卓平台上,IMPL.onStartNestedScroll()调用的实际上是ViewParentCompatLollipopImpl的方法。 点击查看ViewParentCompatLollipopImpl

    //ViewParentCompat 
    static class ViewParentCompatLollipopImpl extends ViewParentCompatKitKatImpl {
        @Override
        public boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            return ViewParentCompatLollipop.onStartNestedScroll(parent, child, target,
                    nestedScrollAxes);
        }

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

        @Override
        public void onStopNestedScroll(ViewParent parent, View target) {
            ViewParentCompatLollipop.onStopNestedScroll(parent, target);
        }

        @Override
        public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed) {
            ViewParentCompatLollipop.onNestedScroll(parent, target, dxConsumed, dyConsumed,
                    dxUnconsumed, dyUnconsumed);
        }

        @Override
        public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
                int[] consumed) {
            ViewParentCompatLollipop.onNestedPreScroll(parent, target, dx, dy, consumed);
        }

        @Override
        public boolean onNestedFling(ViewParent parent, View target, float velocityX,
                float velocityY, boolean consumed) {
            return ViewParentCompatLollipop.onNestedFling(parent, target, velocityX, velocityY,
                    consumed);
        }

        @Override
        public boolean onNestedPreFling(ViewParent parent, View target, float velocityX,
                float velocityY) {
            return ViewParentCompatLollipop.onNestedPreFling(parent, target, velocityX, velocityY);
        }
    }

找到onStartNestedScroll(),实际上调用的是ViewParentCompatLollipop.onStartNestedScroll(),点击去:

    // ViewParentCompatLollipop
    public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
        try {
            return parent.onStartNestedScroll(child, target, nestedScrollAxes);
        } catch (AbstractMethodError e) {
            Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                    "method onStartNestedScroll", e);
            return false;
        }
    }

所以在NestedScrollingChildHelper中使用startNestedScroll()会通过ViewParentCompat.onStartNestedScroll()方法最终调用到ViewParent.onStartNestedScroll(),没有线程切换,没有任何逻辑,一条道走到黑。

其他也是类似,现在能明白为什么子View调用dispatchXXX之后父View能够回调onXXX了吧?

剩下的就不多讲了。

事件分发和NestedScrolling对比

事件分发和NestedScrolling有很多相似、关联之处,但是也有很多不同,为了加深理解,我们可以对比来理解记忆一下。

处理对象

  • 事件分发,分发的是触摸事件(MotionEvent event)

  • NestedScrolling分发的是滚动距离(int deltaY)。

主动权

  • 父View主动权:外部拦截 > 内部拦截 > NestedScrolling

  • 子View主动权:外部拦截 < 内部拦截 < NestedScrolling

拦截滚动

  • 事件分发:一锤子买卖,拦截之后同一序列中的后续所有的事件都由拦截者处理,滚到滑动边界时会卡顿,同方向直到手指重新抬起落下才会继续滚动

  • NestedScrolling:交互式拦截,父View和子View可以互相商量,任意分配滚动事件,滚到滑动边界时过渡很流畅

使用场景

  • 在解决事件冲突和一些特定场景方面,NestedScrolling比事件分发自由度更大,更实用(比如悬浮置顶,SwipeRefreshLayout、CoordinatorLayout等)

  • 在纯事件分发领域(比如拖拽、侧滑、阻尼滚动等),事件分发更实用

实战

请参考 NestedScrollingDemo,里面包含了FrameLayout + LinearLayout以及 WebView + RecyclerView实现的嵌套滚动的demo。

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值