Flutter多控件滑动事件联动(滑动冲突处理)

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) {