Flutter多控件滑动事件联动(滑动冲突处理)
本文以各种方案的原理讲解为主
需要Demo或者源码的同学可以看下面这篇文章:
产品内有多个Flutter页面需要实现下拉关闭效果,因此考虑做一个通用的控件包裹需要下拉关闭页面的内容。最初的实现方法是使用GestureDetector监控滑动手势,使用Transform根据手势移动页面内容。
但是测试中发现如果页面内容包含可上下滑动的ScrollView时,外部控件不会响应手势动作。从Flutter手势竞争的原理分析问题的原因是内外两个滑动控件都通过Hit Test后加入竞技场,最终内层控件胜出,所以后续的事件只在内层控件处理。
我们希望的效果是内部ScrollView的内容滑动到边界后整个页面开始滑动,整个过程是一个滑动事件完成的。为了实现这个效果提出两个方案。
方案1:监听内层ScrollView的ScrollNotification
ScrollView的滑动过程中会发送ScrollNotification。因此,可以通过接收这个通知判断ScrollView的滑动状态,并在合适的时机触发外层控件的滑动。具体解决方案分两种场景:
内层没有ScrollView
仍然使用GestureDetector检测手势并控制整个页面的移动
内层有ScrollView
首先定义一个开关表示外层控件是否需要滑动(needDrag),默认是false。
然后根据收到的不同ScrollNotification做特定处理
- 收到ScrollStartNotification ,表示ScrollView开始滑动,这时needDrag设置为false,优先保证ScrollView的滑动。
- 收到ScrollUpdateNotification ,表示ScrollView的内容开始滚动,这时会通知外层控件,但是因为开关是关闭的,所以外层控件不滑动。
- 收到OverscrollNotification ,表示ScrollView滑动到了边界。例如ListView向下滑动到边界,这时需要触发外层控件开始滑动,因此把needDrag设置为true。外层控件会整体向下移动。
- 这时如果再收到ScrollUpdateNotification 表示手指开始反向移动。因为此时开关是打开的,所以外层控件也会跟随手指反向移动。
- 收到ScrollEndNotification ,表示手指抬起,这时需要判断外层控件的位置,如果下拉超过1/3,则执行退出动画,如果没有到1/3则执行控件回到初始位置的动画。
方案1 的问题
测试中发现,当内层是一个WebView并且内容很短,整个内容不需要上下滚动时,内层控件不会发送ScrollNotification通知。因此方案1在这种场景下不可行。
方案2:自定义Recognizer
方案1 的问题是单纯依赖ScrollNotification在特殊场景下收不到通知。因此我们需要一个在内部ScrollView竞争胜出,但又不发送ScrollNotification的场景下可以处理touch事件并移动页面的方案。思路是自定义一个Recognizer并在竞技场单方面中胜出,从而实现和内部ScrollView同时获取手势的处理权。
获取手势处理权
class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
因为要保证内层控件的Scroll行为优先,因此需要先让内层控件胜出,_MyVerticalDragGestureRecognizer 在被通知失败后单方面宣布自己胜出,从而在不影响内层控件的前提下把自己也加入到手势事件的处理中。
Recognizer加入竞技场
Listener(
child: …,
onPointerDown: (event) {
_recognizer.addPointer(event);
),
这样不论内层是否有ScrollView,我们的Recognizer都可以处理手势事件了。但是当内层的ScrollView的内容滚动时,外层控件不应该移动,因此还是需要增加判断条件。
控制滑动时机
首先给Recognizer加个开关,默认是打开的。
class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
bool needDrag = true;
…… //开关打开onUpdate回调生效
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
然后监听ScrollStartNotification和OverscrollNotification控制开关,
- 收到ScrollStartNotification时,代表内部ScrollView的内容在滚动,这时关闭开关。
- 收到OverscrollNotification时,代表内部ScrollView滚动到边界,这时打开开关。
NotificationListener<ScrollStartNotification>(
child: NotificationListener<OverscrollNotification>(
child: Listener(
child: ......,
onPointerDown: (event) {
_recognizer.addPointer(event); // 加入Hit Test
onNotification: (OverscrollNotification notification) {
if (notification.metrics.axis == Axis.vertical) {
_recognizer.needDrag = true; // 内部滑动到边界并且是纵向滑动时打开
return false;
onNotification: (ScrollStartNotification notification) {