Skip to content

Flutter 小技巧之快速理解手势逻辑 ​

又到了小技巧系列更新时间,今天我们主要分享 Flutter 里的手势触摸逻辑,其实在很久之前我就写过 《面深入触摸和滑动原理》相关的源码分析文章,但是最近有人说源码分析看不懂,有没有简要好理解的,那么本篇就用更简单的角度,带大家理解 Flutter 里的手势相关逻辑。

GestureDetector ​

不管你用 InkWell 、InkResponse 、TextButton 还是 ElevatedButton , 它们针对手势的处理逻辑都是来自于 GestureDetector ,也就是理解 Flutter 的手势处理逻辑入门,核心可以从分析 GestureDetector 开始。

其实更严格意义上讲,手势事件是来自 Listener , GestureDetector 是针对 Listener 进行了封装,只是为了避免复杂的源码分析,这里就不做展开,你可以简单理解为:并不是所有的控件都会响应手势,只有带有 Listener 的才会响应,这主要体现在触摸事件的递归响应上。

在 GestureDetector 里关于事件的响应逻辑主要来自于各种 GestureRecognizer (手势识别)的实现逻辑,不同的手势识别逻辑会响应不同手势结果,相互竞争,最后在 GestureArenaManager (竞技场) 决定出一个胜利者。

简单来说,在竞技场里手势基本遵循两个逻辑:

  • 每个 Recognizer 都可以随时选择失败退出,当竞技场只有它一个的时候它就赢了
  • 每个 Recognizer 都可以随时宣布自己获得胜利,这时其他 Recognizer 也将不再响应

那么如下图所示,在 GestureDetector 里主要有这 8 种 GestureRecognizer 在处理不同的场景,他们会根据用户使用 GestureDetector 时的参数搭配来参与到事件竞技场里。

举个例子,当你使用了 GestureDetector 并配置了 onTap 、onLongPress 和 onDoubleTap ,它们是如何分别响应手势事件的?

这里的核心逻辑就在于 deadline (时间) 的处理,不管是 onLongPress 还是 onDoubleTap 都是靠 deadline 来判断胜负。

例如,当用户只是普通点击时,如下代码所示,因为默认 LongPressGestureRecognizer 的 deadline 是 500 毫秒,所以在定时器达到 500ms 响应之前,就会因为 PointerUpEvent 导致长按定时器停止,无法触发响应长按事件。

反之如果没有 PointerUpEvent 产生,那么 500 ms 之后 LongPressGestureRecognizer 就会响应,直接宣布胜利(accepted)。

默认情况下 GestureDetector 是不支持修改 deadline ,只有直接使用 LongPressGestureRecognizer 时才可以修改 deadline 的时长。

类似的逻辑在 DoubleTapGestureRecognizer 下也有,DoubleTap 的 deadline 是 300 毫秒,当用户首次点击时会注册一个定时器,如果 300 毫秒以内用户没有产生新的点击,那么 DoubleTapGestureRecognizer 就会宣布“失败“退出竞技,反之如果在 300 毫秒内有新的点击,则直接宣布“获胜”,响应 DoubleTap 回调。

那这时候有人就要问了:“在 DoubleTap 过程中,为什么不会触发 onTap” ? 这就需要说到 TapGestureRecognizer 的触发逻辑。

继续前面 GestureDetector 并配置了 onTap 、onLongPress 和 onDoubleTap 的例子,在用户只做普通点击的时候,前面说过:

  • LongPressGestureRecognizer 的定时器 deadline 还没到 500 毫秒会因为 Up 事件而导致失败退出
  • DoubleTapGestureRecognizer 会因为定时器超过 deadline 300 毫秒,没有下一个点击而宣布退出

那么在 Long 和 Double 都失败的情况下,此时 GestureArenaManager (竞技场) 里的成员就只有 TapGestureRecognizer ,这时候竞技场会 close ,会触发竞技场的 sweep 逻辑,直接让最后剩下来的 Recognizer “胜利”,响应 onTap 事件。

所以 TapGestureRecognizer 靠的是胜者为王。

所以基于这个例子,配合一开始说的两个逻辑,就可以直观的理解 Flutter 手势竞技场里的响应逻辑和关键 deadline 的作用。

多个 GestureDetector ​

那么前面都是只有一个 GestureDetector 的场景,如果有两个呢?如下代码所示,在嵌套两个 GestureDetector 下,它们的响应逻辑会是怎么样的?

当区域内有两个 GestureDetector 的时候,用户在普通点击时,因为 deadline 影响,依旧会是在竞技场 close 时才响应 onTap , 但是不同在于此时竞技场里还会有多个 Recognizer 存在,这时候只有排在列表的第一个的 Recognizer 可以赢得事件,也就是上门代码里的红色 200x200 小方块。

因为对于多个 GestureDetector 的情况, Recognizer 在竞技场列表(List<GestureArenaMember)里的顺序和 HitTest 时的递归调用有关系,简单说就是:递归调用会就让我们自下而上的得到一个 HitTestResult 列表,代码里最后的 child 会在最上面。

同时对于单个 GestureDetector 而言,TapGestureRecognizer 会是 _recognizers 的第一个,所以 first 会是响应了 TapGestureRecognizer ,详细逻辑可以看 《面深入触摸和滑动原理》 。

所以简单理解:

  • 两个 GestureDetector 在竞技场里的 member 列表排序时,作为 child 的红色 GestureDetector 因为 HitTest 递归会被排前面
  • GestureDetector 内部 TapGestureRecognizer 会在其内部 _recognizers 排第一

所以 member.first 最终响应了 TapGestureRecognizer ,回到上面两个定律,如果结合多个 GestureDetector 的场景,就应该是:

  • 每个 Recognizer 都可以随时选择失败退出,当竞技场只有它一个的时候它就赢了;如果不止一个,那么在竞技场 close 时, member.first 会获得响应
  • 每个 Recognizer 都可以随时宣布自己获得胜利,这是其他 Recognizer 也将不再响应
​

进阶补充 ​

前面简单介绍了 Flutter 的手势响应的基础逻辑,这里再额外补充两个知识点。

首先,当用户在长按的时候, GestureDetector 何时会发出 onTapDown 事件?

这其实就涉及了另外一个 deadline 参数,当用户在长按的时候,Recognizer 还会触发另外一个定时器,然后通过执行 didExceedDeadline 来发出 onTapDown 事件。

那么问题又来了,既然长按会触发 onTapDown 事件,如果点击区域内有两个 TapGestureRecognizer ,长按的时候因为定时器都触发了 didExceedDeadline ,那是不是两个都会收到 onTapDown 事件 ?

答案是:会的!因为定时器都触发了 didExceedDeadline,从而都发出了 onTapDown 事件,所以两个 onTapDown 回调都会执行,但是后续竞争只会有一个控件能响应 onLongPress 。

另外,如果不是长按导致的 Down 事件, 是不会导致两个 GestureDetector 都触发回调 onTapDown 回调。

第二个补充的是 Listener , 如果你还想深入去看 GestureDetector 的实现,你就会发现 GestureDetector 对 Listener 的封装也许和你想象不大一样, 因为 Listener 的封装只用到了 PointerDown ,并没有用到 onPointerUp ,那 GestureDetector 是怎么响应 Up 和 Move 事件?

这就需要说到前面介绍 《面深入触摸和滑动原理》 里的源码分析,但是为了简单,我们这里只说结论:

因为只有响应了 PointerDown 事件,对应的 GestureRecognizer 才能被添加到 GestureBinding 的 PointerRouter 事件路由和 GestureArenaManager 事件竞技中,而后续的 Up 和 Move 事件主要是通过 GestureBinding 来处理。

更简单的说,就是只有响应了 PointerDown 事件,控件的 Recognizer 才能响应后续统一处理的其他手势事件,而其他事件不需要在 Listener 这里获取回调。

结束 ​

那本篇的小技巧到这里就结束了,本篇主要是用更直观和简单的方式,帮助大家理解 Flutter 里的触摸响应逻辑,如果对更详细实现感兴趣,可以结合 《面深入触摸和滑动原理》 帮助理解,如果你还有什么感兴趣或者有疑惑的,欢迎留言评论~

Released under the MIT License.

本站访客 -- 人次 · 总访问量 -- 次