前言
本次分享的主题是Flutter触摸反馈原理,主要带领大家从源码的角度一步步剖析Flutter手势相关概念如手势裁决、手势识别器、手势竞技场为将来我们在开发Flutter相关的应用的时候打下坚实的基础,在开讲之前默认大家都已经有一定的基础并在工作中实际运用到了Flutter,所以本次分享不会分享基础概念,如果有遇到任何问题都可以线下问我,我不确定能帮到你但我会尽我所能去解决问题,OK 下面开始我们今天的主题“从源码角度深入Flutter触摸反馈原理”。
Flutter事件入口
上面这张图是我从网上其它文章截取过来的,对于现阶段而言先不用去管事件是怎么从原生分发到Flutter层的,只需要知道画红框框的步骤,在Flutter中事件分发的入口在 GestureBinding 类中,所以首先需要关注的是 GestureBinding 的初始化过程 GestureBinding#initInstances
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
@override
void initInstances() {
super.initInstances();
_instance = this;
// 看这里
window.onPointerDataPacket = _handlePointerDataPacket;
}
复制代码
在 GestureBinding 的初始化过程中会将 _handlePointerDataPacket 设置为 window.onPointerDataPacket 的回调,这里的 window 是一个与原生交互的非常重要的对象,当事件从原生分发过来的时候最终会调用 window.onPointerDataPacket ,这里由于设置了回调所以我们继续关注 _handlePointerDataPacket 函数的走向就可以啦。
//1
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
//这里的PointerDataPacket是一个点的信息,将原始事件转换为 PointerEvent 并添加到待处理的事件队列里
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked)
_flushPointerEventQueue();
}
//2
void _flushPointerEventQueue() {
while (_pendingPointerEvents.isNotEmpty)
handlePointerEvent(_pendingPointerEvents.removeFirst());
}
//3
void handlePointerEvent(PointerEvent event) {
assert(!locked);
if (resamplingEnabled) {
_resampler.addOrDispatch(event);
_resampler.sample(samplingOffset, _samplingClock);
return;
}
_resampler.stop();
_handlePointerEventImmediately(event);
}
复制代码
1、可以看到该函数中入参了一个 PointerDataPacket ,PointerDataPacket 可以看作是触点对象封装了你手指触摸在屏幕上的点的信息,并将它包装成 PointerEvent 的对象集合并添加到 _pendingPointerEvents(待处理的事件集合)中。
2、如果 _pendingPointerEvents 队列有数据不断的取出队头数据然后执行 handlePointerEvent 函数。
3、这里没什么主要还是看 _handlePointerEventImmediately 函数,不过这得引申出另一个概念“命中测试”
命中测试
GestureBinding#_handlePointerEventImmediately
//GestureBinding#_handlePointerEventImmediately,handlePointerEvent会对每一个事
//件进行处理,并最终执行到 _handlePointerEventImmediately 函数中,该函数主要执行“击中测试”
//以及事件分发的入口函数 dispatchEvent
void _handlePointerEventImmediately(PointerEvent event) {
//用来封装执行命中测试的结果
HitTestResult? hitTestResult;
//仅仅只先关注down事件
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
hitTestResult = HitTestResult();
//命中测试
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
}
//省略代码......
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
}
复制代码
hitTest 主要负责击中测试相关的业务逻辑,但是当你直接点开 hitTest 函数的时候你会发现只是下面的代码
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
复制代码
这里看起来貌似没啥业务呀,实际上第一次看到这里的时候我也糊涂了很久,因为我忽略了 GestureBinding
是一个 mixin
类,所以它可能是作为一个扩展类被粘贴到其它类后面的,这个特性类似于java中的继承,但是要比继承更加灵活,我们继续在源码中找你会发现 GestureBinding 被粘贴到了 RendererBinding
后面,猜测很有可能在 RendererBinding
中重写了 hitTest 函数。
RendererBinding#hitTest
//RendererBinding#hitTest
@override
void hitTest(HitTestResult result, Offset position) {
//1
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
//GestureBinding#hitTest
@override
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
//2 RenderView#hitTest
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null)
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
复制代码
1、调用 RenderView 的 hitTest 函数,RenderView 是绘制树的根节点,你可以把它看作是所有 Widget 的祖先,执行完 RenderView#hitTest 之后在继续调用 super.hitTest(result, position); 也就是执行 GestureBinding#hitTest 将自身添加到 HitTestResult 中
2、RenderView#hitTest
如果存在子节点就继续调用子节点的 hitTest 函数,这里的 child 即 RenderBox
RenderBox#hitTest
//RenderBox#hitTest
bool hitTest(BoxHitTestResult result, { required Offset position }) {
//事件的 position 触点必须在当前组件内
if (_size!.contains(position)) {
//1
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
//2
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
//HitTestResult#add
final List<HitTestEntry> _path;
void add(HitTestEntry entry) {
assert(entry._transform == null);
entry._transform = _lastTransform;
_path.add(entry);
}
复制代码
1、调用 hitTestChildren 不断的遍历 child ,优先判断 children 节点在判断自己,只要 children 节点中有一个返回 true 那么就将自身添加到 HitTestResult 中并返回 true ,由于遍历的顺序是从父节点遍历到子节点,所以如果子节点符合条件会优先被添加到 HitTestResult 中
2、BoxHitTestResult 是 HitTestResult 的子类,调用它的 add 函数(BoxHitTestResult 没有重写 HitTestResult#add , 所以这里调用的是 HitTestResult 的 add 函数),将触点信息封装到 BoxHitTestEntry 中并塞到 _path 集合中
RenderBox#hitTestChildren
在遍历子节点的时候会遇到该控件是单节点还是多节点的,比如说 Center 就是典型的单节点,Row 就是多节点的控件,判断的依据就是看它们的孩子节点是一个还是多个,hitTestChildren 在 RenderBox 中是一个空实现,所以我们就具体看看它的实现类是如何处理单节点和多节点的区别,单节点以 Center 举例子
//1
class Center extends Align {
const Center({ Key? key, double? widthFactor, double? heightFactor, Widget? child })
: super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
//2 Align#createRenderObject
@override
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.maybeOf(context),
);
}
复制代码
Center 只是一个空壳子,它继承自 Align ,所以我们关注的重点还是看 Align 这个组件,查看 Align 你会发现它继承自 SingleChildRenderObjectWidget ,这里我就有点熟悉了,之前复习 Flutter 三棵树的时候经常碰到这玩意,基本看到它就需要联想到重写了 createRenderObject
这个函数。
//Align#createRenderObject
@override
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.maybeOf(context),
);
}
//RenderShiftedBox#hitTestChildren
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset? transformed) {
assert(transformed == position - childParentData.offset);
return child!.hitTest(result, position: transformed!);
},
);
}
return false;
}
复制代码
Align 对应的 RenderBox
是 RenderPositionedBox ,一层一层的往上找你会发现 RenderPositionedBox 重写 hitTestChildren 的父类是 RenderShiftedBox
,在 result.addWithPaintOffset
内部你会发现最终还是调用的 RenderBox#hitTest 执行子节点的命中测试继续上面的步骤,单节点其实非常容易看懂只要还有子节点并且你的触点信息包含在Widget范围之内就一层一层的迭代下去,多节点的情况我们用 Row 来举例子,第一步还是查看 createRenderObject 函数,如果自身没有就从父类中去查找,最终我们在它的父类 Flex 中查找到 createRenderObject
,接下里的重点就是看它的返回类 RenderFlex 中的 hitTestChildren 方法。
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
return defaultHitTestChildren(result, position: position);
}
bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
//1、定义一个 child 变量
ChildType? child = lastChild;
while (child != null) {
final ParentDataType childParentData = child.parentData! as ParentDataType;
//2 判断是否命中
final bool isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset? transformed) {
assert(transformed == position - childParentData.offset);
return child!.hitTest(result, position: transformed!);
},
);
if (isHit)
return true;
child = childParentData.previousSibling;
}
return false;
}
复制代码
1、hitTestChildren 中的主要逻辑都在 defaultHitTestChildren 中,Flex 是多节点的所以首先定义一个变量用来保存组件集合
中最后一个 child
2、因为是一个“组件集合”,所以这里会循环遍历每一个子节点直到找到可以响应命中测试的那个控件 ,这里的第二步继续往下走就和之前讲到的单节点的遍历一样了,所以这里我们会发现在多节点的情况下只要有一个响应命中测试那么就会返回 true ,其它节点就不会在遍历了并且会继续迭代那个响应初触点的控件,如下图,截止到目前为止我们讲的其实都只是按下 down 的操作,而这一步操作会在接下来的命中测试中帮我们维护一个 HitTestResult#_path
集合,距离我们最近的那个控件的触点信息会首先被封装成 HitTestEntry 添加到集合中,依次类推,最后被添加的就是 GestureBinding 了,而这就是命中测试的目的。
事件分发
前面我巴拉巴拉讲完了命中测试,但我们目前为止都是在 PointerDownEvent
的前提下讲的,那么其它事件呢?我们把之前 _handlePointerEventImmediately 函数中的代码在拿出来翻下
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
hitTestResult = HitTestResult();
//击中测试 - 在前文中已经讲过了
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
//1
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
//2
hitTestResult = _hitTests[event.pointer];
}
//3
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
}
复制代码
1、如果事件是 PointerUpEvent 或者 PointerCancelEvent 从 _hitTests 中移除它并且返回当前的 hitTestResult ,事件依然会继续执行到 3 处的 dispatchEvent 但是这个事件流程也就结束了,这个也很好理解 up 和 cancel 必然代表着一个事件序列的结束
2、这里我查看其它的文章解释是“比如move事件的时候,手指一直是down的”,我有点糊涂不太理解什么意思,暂将它理解为如果是其它的事件(貌似只有move了)那么将通过 event.pointer 获取到之前的 HitTestResult 对象,也就是说 move 事件最终分发的时候还是利用的 _hitTests 将之前 PointerDownEvent 的时候保存下来的 HitTestResult 对象,我是这里理解的,如果出错了还请各位大佬们指点下。
3、如果 hitTestResult 不为空就继续执行 dispatchEvent 函数进行事件的分发
dispatchEvent
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
assert(!locked);
if (hitTestResult == null) {
assert(event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
//......
}
return;
}
//1
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
//......
}
}
}
//2
class BoxHitTestEntry extends HitTestEntry {
BoxHitTestEntry(RenderBox target, this.localPosition)
: assert(localPosition != null),
super(target);
@override
RenderBox get target => super.target as RenderBox;
final Offset localPosition;
}
复制代码
1、遍历 hitTestResult
, HitTestEntry 好理解,每次命中测试的时候符合条件都会封装一些信息到 HitTestEntry 中维护到 hitTestResult 中,而 entry.target 则是 HitTestTarget
,这里建议好好看下代码,为啥是 HitTestTarget ? 因为真正塞到 HitTestResult#_path 中的是 HitTestEntry 的子类 BoxHitTestEntry
往下继续看代码2
2、BoxHitTestEntry 的构造函数中第一个入参就是 RenderBox ,而 RenderBox 继承自 RenderObject,RenderObject 又实现了 HitTestTarget,所以前面的 entry.target.handleEvent 可以理解为是调用处理这个触点的 RenderBox#handleEvent
函数,不是所有的控件都会处理 handleEvent 函数,一般是 RenderPointerListener 处理 handleEvent 函数,RenderPointerListener 在 Listener 组件的 createRenderObject 中被创建,Listener 组件在 RawGestureDetector#RawGestureDetectorState 中的 build 函数中被创建,handleEvent 会根据不同的事件类型回调到RawGestureDetector的相关手势处理
Flutter中的手势
认识手势识别器和手势识别器工厂
在前文中我们了解了命中测试以及事件分发,我想大家脑海中肯定对 Flutter事件分发有了一个基本的概念,在前文的结尾我们引出了手势
这个概念,那么什么是手势呢?点击,双击,滑动,缩放这些都可以被称作是手势,在Flutter中不同的手势由不同的手势识别器处理,为了更好的讲解手势识别器我们从我们最熟悉的一个控件 GestureDetector
入手,GestureDetector 相信大家都比较熟悉,通过这个 Widget 我们可以处理单击,双击,长按,以及滑动的需求,毫不夸张的说,GestureDetector 可以解决我们业务中 80% 与手势相关的业务操作,主要原因呢是因为 GestureDetector 封装了 8 大手势识别器基本涵盖了我们日常开发需要,首先 GestureDetector 是一个 StatelessWidget ,所以我们只需要关注它的的 build 函数。
@override
Widget build(BuildContext context) {
//1
final Map<Type, GestureRecognizerFactory> gestures = <
Type,
GestureRecognizerFactory>{};
if (onTapDown != null ||
onTapUp != null ||
onTap != null ||
onTapCancel != null ||
onSecondaryTap != null ||
onSecondaryTapDown != null ||
onSecondaryTapUp != null ||
onSecondaryTapCancel != null ||
onTertiaryTapDown != null ||
onTertiaryTapUp != null ||
onTertiaryTapCancel != null
) {
//2
///在这里构建出泛型为 TapGestureRecognizer 的 GestureRecognizerFactoryWithHandlers 对象
gestures[TapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
/// 返回对应类型的手势识别器
() => TapGestureRecognizer(debugOwner: this),
/// 通过该对象的回调对 GestureDetector 中定义的回调进行绑定,
/// GestureDetector 中的回调方法,本质上都是在 手势识别器 中触发的
(TapGestureRecognizer instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
..onTap = onTap
..onTapCancel = onTapCancel
..onSecondaryTap = onSecondaryTap
..onSecondaryTapDown = onSecondaryTapDown
..onSecondaryTapUp = onSecondaryTapUp
..onSecondaryTapCancel = onSecondaryTapCancel
..onTertiaryTapDown = onTertiaryTapDown
..onTertiaryTapUp = onTertiaryTapUp
..onTertiaryTapCancel = onTertiaryTapCancel;
},
);
}
//省略其它的手势识别器。。。。。。
//GestureDetector 内部封装了8大手势识别器,其实已经可以满足绝大部分的使用了,但
//它的功能也仅仅只限与此了,GestureDetector的具体实现最终还是委托给 RawGestureDetector 来使用的。
//对于 RawGestureDetector 组件的使用而言,最关键的就是处理 Map<Type, GestureRecognizerFactory> 这个手势识别器工厂映射。
return RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
}
复制代码
build 函数的代码颇多所以我们只看我们需要看的,由于手势识别器的创建代码大多一致,我们也不可能把所有的手势识别器都看一遍,所以接下来我们只关注“ 单击手势识别器 TapGestureRecognizer
”
1、首先我们创建了一个 Map<Type, GestureRecognizerFactory> gestures 的映射集合,Key 为 Type,关于这个 Type 可以理解为 dart 中类型的运行时表示形式, Value 为 GestureRecognizerFactory 即手势识别器工厂。
2、将手势识别器工厂对象保存到 gestures 中,注意这里的 Key 的类型为具体的手势识别器,Value 为泛型是 TapGestureRecognizer 的 GestureRecognizerFactoryWithHandlers 对象,看到这里你可以对这一大坨代码有点不感冒,没关系截止到目前为止我们知道在 GestureDetector 的 build 函数主要做的操作就是将生产具体手势识别器的那个工厂保存到 gestures 集合中,接下来我们就看下手势识别器工厂是个什么玩意。
//gesture_detector.dart # GestureRecognizerFactoryWithHandlers
class GestureRecognizerFactoryWithHandlers<T extends GestureRecognizer>
extends GestureRecognizerFactory<T> {
///在构造函数中暴露出 _constructor _initializer 回调给用户自己处理
///_constructor 主要用于创建手势识别器并返回
///_initializer 回调出手势识别器供用户使用,这里更多的是用于绑定监听
const GestureRecognizerFactoryWithHandlers(this._constructor,
this._initializer)
: assert(_constructor != null),
assert(_initializer != null);
final GestureRecognizerFactoryConstructor<T> _constructor;
final GestureRecognizerFactoryInitializer<T> _initializer;
@override
T constructor() => _constructor();
@override
void initializer(T instance) => _initializer(instance);
}
//gesture_detector.dart # GestureRecognizerFactory
//GestureRecognizerFactory本身是一个抽象类,这说明他不能实例化使用
//GestureRecognizerFactory 可以接受GestureRecognizer类型的泛型
//在Flutter中 GestureRecognizerFactoryWithHandlers 是 GestureRecognizerFactory 的具体实现子类
@optionalTypeArgs
abstract class GestureRecognizerFactory<T extends GestureRecognizer> {
const GestureRecognizerFactory();
//用来构建手势识别器
T constructor();
//用于回调出手势识别器供用户使用
void initializer(T instance);
}
复制代码
可以看到 GestureRecognizerFactoryWithHandlers 派生于 GestureRecognizerFactory,是具体的手势识别器工厂类,重写了 constructor 和 initializer 函数,但是没有写具体的逻辑,而是把逻辑处理交给了 _constructor() 和 _initializer(instance) 函数
,但是你会发现有趣的是这两个方法的最终构建是在 GestureRecognizerFactoryWithHandlers 的构造函数中,所以回到之前 GestureDector 的 build 函数看下下面的截图
我们传入的泛型类型是 TapGestureRecognizer 说明这个工厂类最终创建的是一个单击手势识别器,在代码1
处执行的就是 GestureRecognizerFactoryWithHandlers 的 constructor 函数用于生成手势识别器对象,代码2
处执行的是 _initializer(instance) 函数,用于执行回调绑定的操作,你会发现手势识别器其实已经实现了手势回调,比如说 onTap 的回调最终就是交给 TapGestureRecognizer 来处理的,手势识别器工厂的作用就是“创建手势是识别器以及回调绑定”。
认识 RawGestureDetector
前文中我们已经认识了手势识别器工厂类,在 GestureDetector 的 build 函数的结尾返回的是一个 RawGestureDetector ,RawGestureDetector 是一个 StatefulWidget 这表明他会通过 State 类来维护组件状态,并根据状态生命周期的回调函数进行处理,构造组件的任务将由对应的 State#build 承担,对于StatefulWidget 的子类而言,组件本身只是记录属性信息,我们只需要了解可以设置哪些属性,源码分析的重点在于该 State 类 如何维护状态和构建组件,RawGestureDetector 中分析的重点在 RawGestureDetectorState
,以及 _syncAll(widget.gestures);
函数。
// 只是在对应的状态类中通过 _syncAll 方法,将使用者提供的 gestures 对成员
// _recognizers 进行维护;以及使用 Listener 组件,在按下时,
// 通过 _handlePointerDown 方法将 _recognizers 中的手势识别器和手势事件数据进行关联
/// State for a [RawGestureDetector].
class RawGestureDetectorState extends State<RawGestureDetector> {
//初始化一个 _recognizers map集合
Map<Type, GestureRecognizer>? _recognizers = const <Type, GestureRecognizer>{};
SemanticsGestureDelegate? _semantics;
@override
void initState() {
super.initState();
// _syncAll 函数对 gestures 集合做处理
_syncAll(widget.gestures);
}
@override
void didUpdateWidget(RawGestureDetector oldWidget) {
super.didUpdateWidget(oldWidget);
///当外界调用 setState 而触发本状态的 didUpdateWidget 时,也会进行 _syncAll 中的逻辑
_syncAll(widget.gestures);
}
// 在 dispose 回调中,会依次销毁 _recognizers 中的所有的手势检测器。
// 这也是 RawGestureDetector 组件需要是 StatefulWidget 的原因: GestureRecognizer 对象是需要被销毁的。
@override
void dispose() {
for (final GestureRecognizer recognizer in _recognizers!.values)
recognizer.dispose();
_recognizers = null;
super.dispose();
}
//在 RawGestureDetectorState 初始化的时候,就会创建相关的 手势检测器 且 绑定监听 。
// 这些逻辑在 RawGestureDetectorState#_syncAll 方法中进行。
void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
assert(_recognizers != null);
///1、将状态类中维护的 _recognizers 保存为局部变量 oldRecognizers,而这个 _recognizers
///在第一次使用的时候是一个空集合所以此刻 oldRecognizers 也是一个空集合
final Map<Type, GestureRecognizer> oldRecognizers = _recognizers!;
///重新初始化 _recognizers 成员,为空映射 {}。
_recognizers = <Type, GestureRecognizer>{};
//遍历传入的 gestures,为 _recognizers 中添加成员。
for (final Type type in gestures.keys) {
///正式开始处理 gestures 调用手势识别器工厂对象 constructor 函数返回手势识别器填充到 _recognizers中
///如果 oldRecognizers 已经存在对应类型的 GestureRecognizer,就会直接复用,而不时创建新的。这便是同步所有的手势检测器。
_recognizers![type] =
oldRecognizers[type] ?? gestures[type]!.constructor();
assert(_recognizers![type].runtimeType ==
type, 'GestureRecognizerFactory of type $type created a GestureRecognizer of type ${_recognizers![type]
.runtimeType}. The GestureRecognizerFactory must be specialized with the type of the class that it returns from its constructor method.');
///调用手势识别器工厂类的 initializer 函数绑定监听
gestures[type]!.initializer(_recognizers![type]!);
}
// 如果oldRecognizers 中有_recognizers 中不存在的类型,则将其销毁。
for (final Type type in oldRecognizers.keys) {
if (!_recognizers!.containsKey(type))
oldRecognizers[type]!.dispose();
}
}
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
//遍历 _recognizers 中的 手势检测器 对象,并为其添加 PointerDownEvent。
// 手势事件数据 PointerDownEvent 通过 Listener 组件的 onPointerDown 回调出来,
// 在 RawGestureDetectorState 的 _handlePointerDown 中,将PointerDownEvent 添加到 _recognizers 映射的手势识别器中。
for (final GestureRecognizer recognizer in _recognizers!.values)
recognizer.addPointer(event);
}
@override
Widget build(BuildContext context) {
//RawGestureDetector 的核心最终还是 Listener组件
Widget result = Listener(
//手势检测器 GestureRecognizer 是和 RawGestureDetector 相关,和 Listener 组件没有直接关系。
//在你点击按钮的时候会触发 onPointerDown 回调
onPointerDown: _handlePointerDown,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child,
);
return result;
}
复制代码
相关代码的注释我已经标注在了上面,相信对着这些注释看会轻松很多,同时在 RawGestureDetector 的 build 函数返回值返回的是一个 Listener
组件,看到这个 Listener 组件是不是有点熟悉,在前文讲 dispatchEvent 这一小节的时候有提到这个 Listener 组件,在事件分发时那些 handleEvent 函数最终会被交给 Listener 组件的 RenderPointerListener#handleEvent 处理,GestureDetector 负责生产手势识别器,RawGestureDetector 负责处理手势识别器以及实现监听绑定的操作,而最终这些手势识别器都会被交给 _handlePointerDown
处理,_handlePointerDown 函数里最终也是遍历手势识别器并调用 GestureRecognizer#addPointer 函数,接下来讲解一个点击事件的流程,但是在讲解之前我们还是要先了解下 Flutter手势处理相关概念
。
Flutter手势处理相关概念
GestureArenaMember
//参赛者 GestureArenaMember
abstract class GestureArenaMember {
//竞技成功的回调
void acceptGesture(int pointer);
//竞技失败的回调
void rejectGesture(int pointer);
}
复制代码
GestureArenaEntry
/// 竞技场信息发送器
/// 这个类是 参赛者和竞技场管理者沟通的桥梁,
/// 在手势检测器中会直接或间接地维护该类对象,并通过这个类对象,向竞技场管理者发送想要获胜或想要失败的信息,从而触发 竞技场管理者 的裁断方法。
class GestureArenaEntry {
//私有化构造所以只能被 arena.dart 文件内的类构造
GestureArenaEntry._(this._arena, this._pointer, this._member);
//手势竞技场管理者
final GestureArenaManager _arena;
//触点id
final int _pointer;
//参赛者 - 手势识别器
final GestureArenaMember _member;
//发送手势被拒绝还是被接受的消息
void resolve(GestureDisposition disposition) {
//调用竞技场管理者的 _resolve 函数
_arena._resolve(_pointer, _member, disposition);
}
}
复制代码
_GestureArena
class _GestureArena {
//参赛者列表
final List<GestureArenaMember> members = <GestureArenaMember>[];
//竞技场是否开放
bool isOpen = true;
//表示该竞技场是否挂起
bool isHeld = false;
//表示该竞技场是否等待清扫
bool hasPendingSweep = false;
//渴望胜利的参赛者
GestureArenaMember? eagerWinner;
void add(GestureArenaMember member) {
assert(isOpen);
members.add(member);
}
}
复制代码
GestureArenaManager
///竞技场管理者 - 进行所有的裁决逻辑
///竞技场管理者在 [GestureBinding] 中实例化
///1、GestureBinding 中用到的 mixin 类似继承但是用"粘贴"更贴切一点
class GestureArenaManager {
///key为触点id value为竞技场对象 , 竞技场管理者并非只是管理一个竞技场而是多个,每个触点都会对应一个竞技场
final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
///维护 _arenas 并将参赛者添加到 _arenas 中
///add 函数的调用可以在分析单击手势识别器中查看 [OneSequenceGestureRecognizer]
///-> startTrackingPointer -> _addPointerToArena -> GestureBinding.instance!.gestureArena.add(pointer, this);
GestureArenaEntry add(int pointer, GestureArenaMember member) {
/// putIfAbsent 是 Map 类的一个方法,第二参是一个返回 value 值的函数对象,
/// 且只有当 key 不存在,才会添加该映射元素。该方法最终会返回 key 对应的 value。
final _GestureArena state = _arenas.putIfAbsent(pointer, () {
assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
return _GestureArena();
});
//添加竞赛者
state.add(member);
assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
//创建并返回竞技场信息发送器
return GestureArenaEntry._(this, pointer, member);
}
//竞技场关闭
void close(int pointer) {
//根据触点id获取竞技场对象
final _GestureArena? state = _arenas[pointer];
if (state == null)
return; // This arena either never existed or has been resolved.
//讲是否开关的状态设置为 false 关闭竞技场
state.isOpen = false;
assert(_debugLogDiagnostic(pointer, 'Closing', state));
//尝试执行裁决
_tryToResolveArena(pointer, state);
}
//竞技场清扫函数
void sweep(int pointer) {
//根据触点id获取竞技场对象
final _GestureArena? state = _arenas[pointer];
//如果为null直接return
if (state == null)
return; // This arena either never existed or has been resolved.
//断言,竞技场必须处于打开状态
assert(!state.isOpen);
//如果竞技场处于挂起状态,将它的 hasPendingSweep 设置为 true并返回
if (state.isHeld) {
state.hasPendingSweep = true;
assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
return; // This arena is being held for a long-lived member.
}
assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
//处于非挂起状态时,根据触点ID移除 _arenas 对应竞技场
_arenas.remove(pointer);
if (state.members.isNotEmpty) {
//竞技场中有参赛者执行第一个参赛者 acceptGesture 函数也就是裁决第一个参赛者竞技成功
// First member wins.
assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
state.members.first.acceptGesture(pointer);
//其余的参赛者回调通知竞技失败的消息
for (int i = 1; i < state.members.length; i++)
state.members[i].rejectGesture(pointer);
}
}
//竞技场挂起函数
void hold(int pointer) {
//通过触点id来获取对应的竞技场对象,如果该竞技场存在的话将 isHeld 设置为 true 否则直接返回
final _GestureArena? state = _arenas[pointer];
if (state == null)
return; // This arena either never existed or has been resolved.
state.isHeld = true;
assert(_debugLogDiagnostic(pointer, 'Holding', state));
}
//竞技场释放操作
void release(int pointer) {
//依旧是根据触点id来获取竞技场对象
final _GestureArena? state = _arenas[pointer];
//不存在直接返回
if (state == null)
return; // This arena either never existed or has been resolved.
state.isHeld = false;//将竞技场的挂起状态设置为false
assert(_debugLogDiagnostic(pointer, 'Releasing', state));
//结合 sweep 中的代码如果之前执行竞技场清扫的时候竞技场处于挂起状态 hasPendingSweep 的值会被设置为 true
//当在触发 release 方法的时候会在执行一遍 sweep 函数,这时候会判定第一个参赛者胜利
if (state.hasPendingSweep)
sweep(pointer);
}
//执行裁决 - 根据 GestureDisposition 的情况对入参的参赛者进行拒绝或者接纳
//当竞技场未关闭,且 disposition 为 accepted 时,如果是 accepted ,竞技场已经关闭,会直接通过 _resolveInFavorOf 宣布该参赛者获胜
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
final _GestureArena? state = _arenas[pointer];
if (state == null)
return; // This arena has already resolved.
assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
assert(state.members.contains(member));
//GestureDisposition 状态 == 拒绝
if (disposition == GestureDisposition.rejected) {
//从参赛者列表中移除这个参赛者
state.members.remove(member);
//调用这个参赛者手势被拒绝的函数
member.rejectGesture(pointer);
if (!state.isOpen)
//如果竞技场是关闭状态,执行 _tryToResolveArena 去尝试裁决
_tryToResolveArena(pointer, state);
} else {
//GestureDisposition 状态 == 接受
assert(disposition == GestureDisposition.accepted);
if (state.isOpen) {
//竞技场处于打开状态将 member参赛者设置为"渴望胜利的参赛者"
state.eagerWinner ??= member;
} else {
//如果竞技场是关闭状态直接执行 _resolveInFavorOf 将 member 设置为裁决成功
assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
_resolveInFavorOf(pointer, state, member);
}
}
}
/// 尝试裁决 - 遍历竞技场参赛者,将不是入参的参赛者全部拒绝,触发他们的 rejectGesture 方法 ,
/// 然后宣布入参 member 获胜,执行其 acceptGesture 方法。也就是说,当该裁决方法被调用时,就胜负已定, 竞技会结束 。
void _tryToResolveArena(int pointer, _GestureArena state) {
assert(_arenas[pointer] == state);
assert(!state.isOpen);
//当参赛者只有一位的时候会触发 _resolveByDefault 函数来使竞技成功
if (state.members.length == 1) {
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {
//如果没有竞技者直接移除竞技场
_arenas.remove(pointer);
assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
} else if (state.eagerWinner != null) {
//指定了获胜者就直接执行 _resolveInFavorOf 函数裁决
assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
_resolveInFavorOf(pointer, state, state.eagerWinner!);
}
}
///只会处理参赛者只有一位的竞技场
void _resolveByDefault(int pointer, _GestureArena state) {
if (!_arenas.containsKey(pointer))
return; // Already resolved earlier.
assert(_arenas[pointer] == state);
assert(!state.isOpen);
final List<GestureArenaMember> members = state.members;
assert(members.length == 1);
_arenas.remove(pointer);
assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}'));
state.members.first.acceptGesture(pointer);
}
//指定成员获胜
void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
assert(state == _arenas[pointer]);
assert(state != null);
assert(state.eagerWinner == null || state.eagerWinner == member);
assert(!state.isOpen);
//通过触点id移除竞技场对象
_arenas.remove(pointer);
// 遍历竞技场参赛者,将不是入参的参赛者全部拒绝,触发他们的 rejectGesture 方法 ,
// 然后宣布入参 member 获胜,执行其 acceptGesture 方法。也就是说,当该裁决方法被调用时,就胜负已定, 竞技会结束 。
for (final GestureArenaMember rejectedMember in state.members) {
if (rejectedMember != member)
rejectedMember.rejectGesture(pointer);
}
member.acceptGesture(pointer);
}
}
复制代码
TapGestureRecognizer 举例
当有一个 down 事件来临的时候会先去执行 GestureBinding#_handlePointerEventImmediately 进行命中测试相关的逻辑,拿到最终的 HitTestResult ,紧接着走 dispatchEvent 函数进行事件分发,这里前面讲过,在 dispatchEvent 函数里会执行 entry.target.handleEvent(event.transformed(entry.transform), entry); 也就是说调用 RenderObject 的 handleEvent 函数,但是前文也讲了一般只有 RenderPointerListener 处理 handleEvent 函数,所以继续走接下来的流程。
RenderPointerListener#handleEvent 函数中会判断当前点击事件是什么类型的事件,现在由于是按下事件所以会执行 onPointerDown 回调,该回调是由 Listener 组件传递过来的,最终执行的是 Listener 组件中的 onPointerDown 即 _handlePointerDown
,_handlePointerDown 中会调用手势识别器的 addPointer 函数,目前我们只关注 TapGestureRecognizer ,继续下面的流程【GestureRecognizer#addPointer】-> 【PrimaryPointerGestureRecognizer#addAllowedPointer】->【OneSequenceGestureRecognizer#startTrackingPointer】,最终执行到 startTrackingPointer
跟踪触点id。
@protected
void startTrackingPointer(int pointer, [Matrix4? transform]) {
//1
GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform);
_trackedPointers.add(pointer);
//2
_entries[pointer] = _addPointerToArena(pointer);
}
复制代码
1、在 GestureBinding 中会维护一个触点路由对象 pointerRouter ,这里将 handleEvent 作为参数传入触点路由对象,当用户与平台接触的时候事件就会分发给已经注册的手势识别器,触发 handleEvent 来分发事件
2、调用 _addPointerToArena 函数实质上就是将自己添加到手势竞技场中
GestureArenaEntry _addPointerToArena(int pointer) {
if (_team != null)
return _team!.add(pointer, this);
//pointer:触点id this:手势识别器,这里指单击手势识别器
return GestureBinding.instance!.gestureArena.add(pointer, this);
}
复制代码
在你手指按下也就是触发 down 事件的时候会首先进行命中测试,根据命中测试的结果生成 HitTestResult ,然后对其分发,Listener 组件会第一个分发到事件调用 _handlePointerDown ,将 handleEvent 绑定到触点路由,在调用 _addPointerToArena
将自身添加到手势竞技场中,接下来我们看下在代码中是如何添加到手势竞技场的,在 GestureBinding 中会实例化竞技场管理器 GestureArenaManager gestureArena = GestureArenaManager();
调用 _addPointerToArena 执行以下代码:
//如果先前查看了 Flutter 手势相关概念的话,这里在看会稍微轻松点
GestureArenaEntry add(int pointer, GestureArenaMember member) {
/// putIfAbsent 是 Map 类的一个方法,第二参是一个返回 value 值的函数对象,
/// 且只有当 key 不存在,才会添加该映射元素。该方法最终会返回 key 对应的 value。
final _GestureArena state = _arenas.putIfAbsent(pointer, () {
assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
return _GestureArena();
});
//添加竞赛者
state.add(member);
assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
//创建并返回竞技场信息发送器
return GestureArenaEntry._(this, pointer, member);
}
复制代码
首先会去新建一个竞技场 _GestureArena
,将参赛者 GestureArenaMember
添加到竞技场中并返回一个竞技场信息发送器 GestureArenaEntry 对象,查看 OneSequenceGestureRecognizer 代码,你会发现它维护了一个 Map<int, GestureArenaEntry> _entries
用于保存竞技场信息发送器,这样手势检测器也就可以触发 GestureArenaEntry 手势裁决的相关功能,还记得我们之前讲过的事件分发中提到的 RenderPointerListener 都会执行它的 handleEvent 函数,所以以上的逻辑在遍历到其它RawGestureDetector 时都会在走一遍,并且最终会执行 GestureBinding 的 handleEvent 函数。
@override // from HitTestTarget
//GestureBinding#handleEvent
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
复制代码
你会发现如果是 PointerDownEvent 会执行竞技场关闭的逻辑 gestureArena.close(event.pointer);
具体的代码我就不贴了,在上面都有,在这一步中会执行尝试裁决 _tryToResolveArena 函数
/// 尝试裁决 - 遍历竞技场参赛者,将不是入参的参赛者全部拒绝,触发他们的 rejectGesture 方法 ,
/// 然后宣布入参 member 获胜,执行其 acceptGesture 方法。也就是说,当该裁决方法被调用时,就胜负已定, 竞技会结束 。
void _tryToResolveArena(int pointer, _GestureArena state) {
assert(_arenas[pointer] == state);
assert(!state.isOpen);
//当参赛者只有一位的时候会触发 _resolveByDefault 函数来使竞技成功
if (state.members.length == 1) {
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {
//如果没有竞技者直接移除竞技场
_arenas.remove(pointer);
assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
} else if (state.eagerWinner != null) {
//指定了获胜者就直接执行 _resolveInFavorOf 函数裁决
assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
_resolveInFavorOf(pointer, state, state.eagerWinner!);
}
}
复制代码
这时候我们假设只有一个参赛者也就是在你的Widget树上只有一个 RawGestureDetector ,那么此刻可以确定参赛者只有一个也就是会执行 _resolveByDefault 函数,在该函数中会调用参赛者的 acceptGesture 函数,也就是手势识别器竞技成功的回调,但是如果在Widget树上有多个参赛者此刻是裁决不出任何获胜者的。
那么截止到目前触点已经被注册到了手势检测器中了,并且 PrimaryPointerGestureRecognizer#handleEvent 函数会被维护到触点路由 PointerRouter
中,当你抬起手指触发 up 事件的时候会进行事件分发,这时候会执行到 GestureBinding#handleEvent 函数,执行pointerRouter.route(event); 根据你的触点id也就可以获取到你要执行的是哪一个 handleEvent ,这里执行的也就是之前注册的 PrimaryPointerGestureRecognizer#handleEvent
@override
void handleEvent(PointerEvent event) {
assert(state != GestureRecognizerState.ready);
//1
if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
//2
final bool isPreAcceptSlopPastTolerance =
!_gestureAccepted &&
preAcceptSlopTolerance != null &&
_getGlobalDistance(event) > preAcceptSlopTolerance!;
final bool isPostAcceptSlopPastTolerance =
_gestureAccepted &&
postAcceptSlopTolerance != null &&
_getGlobalDistance(event) > postAcceptSlopTolerance!;
//3
if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer!);
} else {
handlePrimaryPointer(event);
}
}
stopTrackingIfPointerNoLongerDown(event);
}
复制代码
1、在进入竞技场的时候参赛者的状态就已经被设置为 possible , 并且当前 event
的触点 id
必须和他一致才能进行 if 代码块的校验逻辑。
2、_gestureAccepted
属性默认为 false
,只会在该参赛者胜利后被置为 true
,preAcceptSlopTolerance
是一个 double
值,标记着触点移动距离的阈值,为 18 逻辑像素
。isPreAcceptSlopPastTolerance
主要就是在竞技还没胜利时,校验加入时的触点坐标和当前 event
的触点距离是否大于 18 逻辑像素,如果大于的话会返回 true
,isPreAcceptSlopPastTolerance 成立的条件是参赛者已经胜利并且触点偏移了18像素。(参考自掘金小册 Flutter手势探索 - 执掌天下)
3、当触点在移动事件时,且上面两个标识有一个为 true
,会执行 resolve
方法,传入 rejected
,表示当前参赛者发送失败请求,将退出竞技,然后通过 stopTrackingPointer
停止追踪触点。这也是为什么点击按下后,滑动一段距离,单击事件会失败的根本原因。
前面讲了那么多,其实也就是讲了单击手势竞技是如何竞技的,但是如何判断一个单击手势是如何胜利的,可以那么理解其它的竞赛者都失败了那么最后一个就胜利了,在按下的时候会将竞技场关闭,但关闭并不意味着后续没有流程了,而是新的开始,在触发 PointerUpEvent 的时候会执行竞技场的清扫流程执行 gestureArena.sweep(event.pointer); 函数,如果竞技场没有处于挂起状态那么就会判定第一位胜利。
那么截止到现在呢,我的这篇分享也就结束了,当然了还有很多东西还没补充,但我实在写不动了,本篇文章主要是我在公司分享用的,后面我把我还没看懂的慢慢给补充上去。