GSYFlutterBook/N15.md

113 lines
9.0 KiB
Markdown
Raw Permalink Normal View History

2023-01-04 09:38:40 +08:00
# Flutter 小技巧之快速理解手势逻辑
又到了小技巧系列更新时间,今天我们主要分享 Flutter 里的手势触摸逻辑,其实在很久之前我就写过 [《面深入触摸和滑动原理》](https://juejin.cn/post/6844903841742192648)相关的源码分析文章,但是最近有人说源码分析看不懂,有没有简要好理解的,**那么本篇就用更简单的角度,带大家理解 Flutter 里的手势相关逻辑**。
# GestureDetector
不管你用 `InkWell` 、`InkResponse` 、`TextButton` 还是 `ElevatedButton` 它们针对手势的处理逻辑都是来自于 `GestureDetector` ,也就是理解 Flutter 的手势处理逻辑入门,核心可以从分析 `GestureDetector` 开始。
> 其实更严格意义上讲,手势事件是来自 `Listener` `GestureDetector` 是针对 `Listener` 进行了封装,只是为了避免复杂的源码分析,这里就不做展开,你可以简单理解为:**并不是所有的控件都会响应手势,只有带有 `Listener` 的才会响应**,这主要体现在触摸事件的递归响应上。
**在 `GestureDetector` 里关于事件的响应逻辑主要来自于各种 `GestureRecognizer` (手势识别)的实现逻辑**,不同的手势识别逻辑会响应不同手势结果,相互竞争,最后在 `GestureArenaManager` (竞技场) 决定出一个胜利者。
简单来说,在竞技场里手势基本遵循两个逻辑:
- **每个 Recognizer 都可以随时选择失败退出,当竞技场只有它一个的时候它就赢了**
- **每个 Recognizer 都可以随时宣布自己获得胜利,这时其他 Recognizer 也将不再响应**
那么如下图所示,在 `GestureDetector` 里主要有这 8 种 `GestureRecognizer` 在处理不同的场景,他们会根据用户使用 `GestureDetector` 时的参数搭配来参与到事件竞技场里。
![](http://img.cdn.guoshuyu.cn/20221215_N15/image1.png)
举个例子,当你使用了 `GestureDetector` 并配置了 `onTap` 、`onLongPress` 和 `onDoubleTap` ,它们是如何分别响应手势事件的?
**这里的核心逻辑就在于 deadline (时间) 的处理,不管是 `onLongPress` 还是 `onDoubleTap` 都是靠 deadline 来判断胜负**。
![](http://img.cdn.guoshuyu.cn/20221215_N15/image2.png)
例如,当用户只是普通点击时,如下代码所示,因为默认 `LongPressGestureRecognizer` 的 deadline 是 500 毫秒,所以**在定时器达到 500ms 响应之前,就会因为 `PointerUpEvent` 导致长按定时器停止,无法触发响应长按事件**。
反之如果没有 `PointerUpEvent` 产生,那么 500 ms 之后 `LongPressGestureRecognizer` 就会响应,直接宣布胜利(accepted)。
![](http://img.cdn.guoshuyu.cn/20221215_N15/image3.png)
> 默认情况下 `GestureDetector` 是不支持修改 deadline ,只有直接使用 `LongPressGestureRecognizer` 时才可以修改 deadline 的时长。
类似的逻辑在 `DoubleTapGestureRecognizer` 下也有DoubleTap 的 deadline 是 300 毫秒,当用户首次点击时会注册一个定时器,**如果 300 毫秒以内用户没有产生新的点击,那么 `DoubleTapGestureRecognizer` 就会宣布“失败“退出竞技**,反之如果在 300 毫秒内有新的点击,则直接宣布“获胜”,响应 DoubleTap 回调。
![](http://img.cdn.guoshuyu.cn/20221215_N15/image4.png)
那这时候有人就要问了:“*在 `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` 下,它们的响应逻辑会是怎么样的?
![](http://img.cdn.guoshuyu.cn/20221215_N15/image5.png)
当区域内有两个 `GestureDetector` 的时候,用户在普通点击时,因为 deadline 影响,依旧会是在竞技场 `close` 时才响应 `onTap` **但是不同在于此时竞技场里还会有多个 `Recognizer` 存在,这时候只有排在列表的第一个的 `Recognizer` 可以赢得事件**,也就是上门代码里的红色 200x200 小方块。
![](http://img.cdn.guoshuyu.cn/20221215_N15/image6.png)
因为对于多个 `GestureDetector` 的情况, `Recognizer` 在竞技场列表(`List<GestureArenaMember`)里的顺序和 `HitTest` 时的递归调用有关系简单说就是**递归调用会就让我们自下而上的得到一个 `HitTestResult` 列表代码里最后的 child 会在最上面**。
> 同时对于单个 `GestureDetector` 而言,`TapGestureRecognizer` 会是 `_recognizers` 的第一个,所以 `first` 会是响应了 `TapGestureRecognizer` ,详细逻辑可以看 [《面深入触摸和滑动原理》](https://juejin.cn/post/6844903841742192648) 。
所以简单理解:
- 两个 `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` 事件。
![](http://img.cdn.guoshuyu.cn/20221215_N15/image7.png)
那么问题又来了,既然长按会触发 `onTapDown` 事件,如果点击区域内有两个 `TapGestureRecognizer` ,长按的时候因为定时器都触发了 `didExceedDeadline` ,那是不是两个都会收到 `onTapDown` 事件
![](http://img.cdn.guoshuyu.cn/20221215_N15/image8.png)
答案是:会的!**因为定时器都触发了 `didExceedDeadline`,从而都发出了 `onTapDown` 事件,所以两个 `onTapDown` 回调都会执行,但是后续竞争只会有一个控件能响应 `onLongPress` 。**
> 另外,如果不是长按导致的 Down 事件, 是不会导致两个 `GestureDetector` 都触发回调 `onTapDown` 回调。
第二个补充的是 `Listener` 如果你还想深入去看 `GestureDetector` 的实现,你就会发现 `GestureDetector``Listener` 的封装也许和你想象不大一样, **因为 `Listener` 的封装只用到了 `PointerDown` ,并没有用到 `onPointerUp`** ,那 `GestureDetector` 是怎么响应 Up 和 Move 事件?
![](http://img.cdn.guoshuyu.cn/20221215_N15/image9.png)
这就需要说到前面介绍 [《面深入触摸和滑动原理》](https://juejin.cn/post/6844903841742192648) 里的源码分析,但是为了简单,我们这里只说结论:
> 因为只有响应了 `PointerDown` 事件,对应的 `GestureRecognizer` 才能被添加到 `GestureBinding` 的 `PointerRouter` 事件路由和 `GestureArenaManager` 事件竞技中,而**后续的 Up 和 Move 事件主要是通过 `GestureBinding` 来处理**。
更简单的说,**就是只有响应了 `PointerDown` 事件,控件的 `Recognizer` 才能响应后续统一处理的其他手势事件,而其他事件不需要在 `Listener` 这里获取回调**。
# 结束
那本篇的小技巧到这里就结束了,本篇主要是用更直观和简单的方式,帮助大家理解 Flutter 里的触摸响应逻辑,如果对更详细实现感兴趣,可以结合 [《面深入触摸和滑动原理》](https://juejin.cn/post/6844903841742192648) 帮助理解,如果你还有什么感兴趣或者有疑惑的,欢迎留言评论~