EventAndNestedScroll

原理&流程

一. ns child会在收到DOWN事件时,找到自己祖上中最近的能与自己匹配的ns parent,与它进行绑定并关闭它的事件拦截机制

二. 然后ns child会在接下来的MOVE事件中判定出用户触发了滑动手势,并把事件流拦截下来给自己消费

三. 消费事件流时,对于每一次MOVE事件增加的滑动距离:

  1. ns child并不是直接自己消费,而是先把它交给ns parent,让ns parent可以在ns child之前消费滑动dispatch/onNestedPreScroll()
  2. 如果ns parent没有消费或是没有消费完,ns child再自己消费剩下的滑动dispatchNestedScroll()
  3. 如果ns child自己还是没有消费完这个滑动,会再把剩下的滑动交给ns parent消费onNestedScroll()
  4. 最后如果滑动还有剩余,ns child可以做最终的处理dispatchNestedScroll()

四. 同时在ns childcomputeScroll()方法中,ns child也会把自己因为用户fling操作引发的滑动,与上一条中用户滑动屏幕触发的滑动一样,使用「parent -> child -> parent -> child」的顺序进行消费

ns child使用更灵活的方式找到和绑定自己的ns parent,而不是直接找自己的上一级结点

ns childDOWN事件时关闭ns parent的事件拦截机制单独用了一个 Flag 进行关闭,这就不会关闭ns parent对其他手势的拦截,也不会递归往上关闭祖上们的事件拦截机制。ns child直到在MOVE事件中确定自己要开始滑动后,才会调用requestDisallowInterceptTouchEvent(true)递归关闭祖上们全部的事件拦截

对每一次MOVE事件传递来的滑动,都使用「parent -> child -> parent -> child」机制进行消费,让ns child在消费滑动时与ns parent配合更加细致、紧密和灵活

对于因为用户fling操作引发的滑动,与用户滑动屏幕触发的滑动使用同样的机制进行消费,实现了完美的惯性连续效果

DOWN事件来时,NSCstartNestedScroll()找到自己祖上匹配的NSP进行绑定并关闭NSP的拦截;

MOVE事件来时:

「parent -> child -> parent -> child」的顺序进行消费

NSCdispatchNestedPreScroll()中触发NSPonNestedPreScroll()预先进行滑动事件的消费,之后将剩余事件返回;接着NSC消费剩余事件;

NSC消费事件后再dispatchNestedScroll()去将剩余事件传递给NSPonNestedScroll()消费。

NSP消费完剩余事件返回NSC,NSC此时可以做最后的处理,比如overScroll效果比如在 fling 的时候停止scroller

MOVE事件中处理滑动按照这个顺序进行消费:「dispatchNestedPreScroll()到Parent -> 自己 -> dispatchNestedScroll() -> 自己」一样,惯性滑动按照上面的顺序。

Case分析

ScrollView嵌套ScrollView

默认情况:外部ScrollView优先获取上下滑动的权力,在蓝色区域上下滑动,内部ScrollView并不会上下滑动

非嵌套滑动.gif

简述:

默认内部ScrollView会消费DOWN事件

默认外部ScrollView优先获取MOVE事件处理机会,并且当yDiff > mTouchSlop时,外部ScrollView会拦截掉MOVE事件并给内部ScrollView发CANCEL事件。

表现:(不考虑后来 Google 给它加的NestedScroll开关),嵌套ScrollView同向滑动的效果如上,内部ScrollView优先获得DOWN事件的处理机会,外部的ScrollView优先获得MOVE事件的处理机会并且拦截(yDiff > mTouchSlop(8,可配置常量))。

分析:

当外部ScrollView嵌套内部ScrollView时,DOWN事件在内部ScrollView的onTouchEvent中返回true,内部ScrollView处理了DOWN事件。MOVE事件首先到达外部ScrollView的onInterceptTouchEvent方法,当滑动距离大于mTouchSlop时,会拦截掉MOVE事件,给内部ScrollView发出CANCEL事件,从而外部ScrollView获得了事件的处理权,内部ScrollView失去了事件的处理权。

ScrollView 实现了ViewParent接口,而ViewParent接口声明了和NestedScrollParent一样的接口方法

ScrollView 继承于View,而View实现了NestedScrollChild接口方法。

image-20220112112026228

(看这就知道了,整体滑动嵌套的思路是NSC先获取事件处理优先权,也许是通过禁止外部拦截的方法获取的,自身的滑动事件消费剩余的滑动距离再由NSC在自身的dispatchNestedXXX中,通知NSP的onNestesXXScroll方法承接,所以内部是主动,外部是被动。至于惯性扔动事件类似?)

如果要达到外层ScrollView可以消费内部溢出的滑动和Fling事件的话,需要外层实现NestedScrollingParent,内层实现NestedScrollingChild协作事件处理。

???:

1、ScrollView覆盖了View的onTouchEvent并默认返回true;所以如果一个ScrollView外部还嵌套了其他ViewGroup了,则Down事件不会返回到外部,也同时MOVE、UP事件也随DOWN事件被ScrollView或其子类内部消费。

2、外ScrollView嵌套内ScrollView,内ScrollView包裹一个内部View的话,不打开(残缺的)嵌套滑动开关setNestedScrollingEnabled(true)的话,点击内部View拖动:

那么DOWN事件会由外ScrollView传到内ScrollView再传到内部View,内部View不消费,回传到内ScrollView消费;

MOVE事件diff超过touchSlop的话会被外ScrollView拦截处理,并产生一个CANCEL事件交给内SCrollView,因为DOWN已经被内ScrollView消费了,所以CANCEL事件也交给了内ScrollView消费,而不会再给到内部View。

UP事件只被外ScrollView分发并传递给外ScrollView的onTouchEvent消费,是的没有拦截。原因未知(理论上应该跟CANCEL一样才对…….)

3、不用小心翼翼地让改动尽量小,既然内部优先,完全可以让内部的ScrollViewDOWN事件的时候就申请外部不拦截,然后在滑动一段距离后,如果判断自己在该滑动方向无法滑动,再取消对外部的拦截限制,像这样也只能实现滑动事件,对于惯性事件的处理是不到位的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SimpleNestedScrollView(context: Context, attrs: AttributeSet) : ScrollView(context, attrs) {
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) parent.requestDisallowInterceptTouchEvent(true)

if (ev.actionMasked == MotionEvent.ACTION_MOVE) {
val offsetY = ev.y.toInt() - getInt("mLastMotionY")

if (Math.abs(offsetY) > getInt("mTouchSlop")) {
if ((offsetY > 0 && isScrollToTop()) || (offsetY < 0 && isScrollToBottom())) {
parent.requestDisallowInterceptTouchEvent(false)
}
}
}

return super.dispatchTouchEvent(ev)
}
}

ScrollView的真实源码实际上是直接在DOWN事件中请求了parent禁止拦截,然后自身先处理消耗MOVE事件直到达到自身的滑动边界之后再找自己的parent处理剩余滑动距离。对于惯性事件也是处理不到位的。

浅析NestedScrolling嵌套滑动机制之基础篇 - 掘金 (juejin.cn)

事件分发四部曲之二《嵌套滑动事件分析》 - 掘金 (juejin.cn)

Author

white crow

Posted on

2022-01-12

Updated on

2024-03-25

Licensed under