type
status
date
slug
summary
tags
category
icon

一、什么是手势冲突?它和 Listener 有啥区别?

要解释这个问题,首先先来定义下什么是手势。flutter 官方对于手势的定义是:
一种语义操作(点击、拖动、缩放)。通常是由一系列单独的指针事件 (pointer event) 组成,也可以是多个单独的指针事件组组成。原文
既然是有语义的,那么避免不了冲突,比如横向拖动和竖向拖动,当手指在屏幕上滑动的时候,我是响应横向滚动呢?还是竖向滚动呢?还是两个一起动?
Listener widget 就是一个指针事件消费者,不存在语义的定义,相当于独行侠,自己处理自己的事件,和其它 Listener 不相干,但是手势就不同了,它不能只考虑自己,这样整个 app 的交互就会乱。

二、Flutter 是怎么解决手势处理的?

Flutter  的手势处理源码分析较为复杂,这里直接先说解决方案
当 Flutter 中存在手势冲突的时候,它是根据 手势竞技场(gesture arena) 来解决这个问题的。所有的 手势识别器 GestureRecongnizer 都是一个 手势竞技场成员(GestureArenaMember
当屏幕上的指定指针有多个 手势识别器 时,框架会让每个 手势识别器 加入到 手势竞技场 来处理手势消歧。手势竞技场 会根据以下规则来判定胜出者:
  • 在任何时候,识别器都可以宣告失败并离开竞技场。如果竞技场中只有一个识别器,那么这个识别器就是胜者。
  • 在任何时候,任何识别器都可以宣告胜利,这将导致这个识别器胜出,其他识别器失败。
假如有两个手势识别器,横向拖动、纵向拖动,当指针 down 落下后,这两个手势识别器都会加入到 手势竞技场 中,观测指针的移动事件 (PointerMoveEvent)。如果用户在横向上移动超过了特定像素,横向识别器会宣告胜利,手势也会被当作横向拖动处理。同样的,如果用户在纵向上移动超过了特定的像素,纵向识别器会宣告胜利。
但是如果 手势竞技场 只有一个手势识别器的话,这个识别器就会被立即胜出,比如只有一个横向拖动识别器的时候,就不需要等到横向移动一定距离后了,可以立即响应横向拖动,非常高效灵敏。

1. 如果有两个竖向拖动识别器是怎么处理的?

这里的处理逻辑其实和 一个横向,一个竖向识别器 一样,存在多个识别器的话需要看哪个识别器先宣告成功,或者主动宣告失败直到还剩一个识别器。
一般来说存在两个竖向拖动识别器是 ListView 嵌套 ListView 的情况,在这种情况下只有子 ListView 会响应,因为指针事件(pointer event)是 child 优先消费的。
到这里为止,关于 flutter 怎么处理手势冲突的解决方案就讲完了,接下来就是枯燥的源码分析环节了。对于只需要了解手势冲突解决方案的,看到这里就 OK 了,对于源码和细节想深究的,一起往下看。

三、源码解析

1. 简单回顾下 pointer event 分发逻辑

在 {% post_link Flutter-手势事件解析(一) 文章一 %} 中讲述了 pointer event 的分发逻辑
notion image
简述就是:pointer down 事件到来后,RendererBinding 会从 RenderObject 树的根开始执行 hitTest 判定,得到 hitTestResult,里面包含了需要消费 pointer event 事件的消费者。然后再将当前的 down 事件和之后的 move / up / cancel 等事件发送给对应的消费者

2. GestureBinding 里面的竞技场管理类 GestureArenaManager

GestureBinding 单例中有个 GestureArenaManager,用于管理手势竞技场,这个管理者是在成员方法 handleEvent(PointerEvent, HitTestEntry) 中被调用的:
  • *那么这个 handleEvent(..) 什么时候会被调用呢?**其实 GestureBinding 自己就是一个 HitTestTarget,也就是它是个 pointer event 消费者,这个在成员方法 hitTest(..) 中可以看出来:
GestureBinding 自身也参与到 hitTest(..) 分发中,并且直接把自己加入到 HitTEstResult 的消费者队列中。根据子类 RendererBinding 的实现看,会先执行 rendererView 的 hitTest,然后才执行 GestureBinding 的 hitTest。
这里可以得出结论的是:GestureBinding 会在 RenderObject 树之后去消费 pointer event。

继续解析 handleEvent(..) 的代码:
  1. 在参数 event 是 PointerDownEvent 的时候,会调用 gestureArena.close(event.pointer)方法,这个方法的注释如下:
也就是当 GestureBinding 收到 down 事件的时候,竞技场的报名就结束了,不允许中途报名( GestureBinding 是在 RenderObject 树之后接受 pointer event 的)。所以我们只有在 down 事件的时候可以添加手势识别器到竞技场中。
最后的 _tryToResolveArena(pointer, state); 是用来得出某些情况下的胜利者的,我们来看下源码:
代码中的可以看到,如果 竞技者 只有一位,那么直接宣告这位 竞技者 胜利,如果没有 竞技者,直接移除当前竞技场,如果有个激进的 获胜者(state.eagerWinner,就是在 down 期间就宣告胜利的 竞技者),则把它判定为获胜者。
  1. 当参数 event 是 PointerUpEvent 的时候,会调用 gestureArena.sweep(event.pointer),这个方法的注释如下:
PointerUpEvent 是一次完整触摸事件的终点,这时候竞技场管理者就开始清扫竞技场了,如果这时候还没有获胜的竞技者,那么第一位将会获胜!
sweep 可以被延迟(这部分代码省略显示了),需要调用 gestureArena.hold(pointer),可以延迟竞技场的清理直到 gestureArena.release(pointer) 被调用
GestureBinding 中关于竞技场处理的逻辑到这里就完了,GestureBinding 只负责了关闭竞技场报名和最后的清理竞技场,并没有关于 PointerMoveEvent 中间过程中的一些竞技判定逻辑。为了查找这部分的逻辑,我们来分析下 GestureDetector 的源码:

GestureDetector 源码解析

GestureDetector 是一个 StalessWidget,它的大部分代码就是把各种手势回调转化为手势识别器,例如将 onHorizontalDragStart 转换为 HorizontalDragGestureRecognizer,然后将这些识别器代理到 RawGestureDetector 中。
RawGestureDetector 是个 StatefulWidget,在它的 build 方法中,我们可以看到 Listener 的身影:
Listener 是监听原始 pointer event 的组件,这里只监听了 onPointerDown 事件,继续往下追踪:
可以看到在 _handlePointerDown 的时候,遍历了所有的 GestureRecognizer 将当前的 down 事件传递了进去。这里和我当初的预期有些差异,我以为这里的 Listener 会监听所有的 down、move、up、cancel,然后代理到 GestureRecognizer 中,看代码并不是。那么手势识别器是在什么时候接受 move、up、cancel 等 pointer event 的呢?
这里就要看下 recognizer.addPointer(event) 的实现了:
。。。啥都没有,继续看下子类的实现:
可以在子类实现中看到,最后会向 GestureBinding 单例中注册接收 pointer event 用的 handler,并且在 GestureBinding 的竞技场管理员报名自己参与竞技。也就是到这为止,手势识别器已经可以收到 pointer event ,并且报名参加了手势竞争。

3. 如何宣告胜利/失败

既然手势识别器可以收到 pointer event 了,那么它就可以随时宣告胜利/失败了,来看下这部分代码:
上述代码是拖动识别器中宣告胜利的代码,宣告胜利后最后会调用到 GestureArenaEntry 中的 resolve 方法。这里 DragGestureRecognizer 会遍历所有的竞技场是因为它是一个 OneSequenceGestureRecognizer,如果存在多个手指触摸事件,会一起处理,要么一起胜利,要么一起失败。
我们继续看下 _arena._resolve(_pointer, _member, disposition) 的实现:
通过源码分析可以知道,_arena._resolve(_pointer, _member, disposition) 会让当前 _member 可能成为胜利者,为什么是可能呢?因为竞技场可能不存在,只要竞技场有胜利者,当前竞技场就会从 GestureArenaManager 移除,之后的竞技者来获取胜利的时候就发现没有竞技场了。
到这里为止,关于手势指针事件相关的内容就都讲完了!源码分析并不是分析所有的源码,不过道路都一样,核心功能都已经分析了,其它的可以自己需要的时候去分析。
谈谈 Flutter 的 buildFlutter 手势事件解析(一)
Loading...