前言
当前案例 Flutter SDK版本:3.13.2
本文对 事件传递只做 简单概述,主要讲解,事件传递过程中可能遇到的问题解决,比如 事件冒泡、事件穿透;
不是我偷懒,是自认为没有这几位写的详细、仔细,非常建议先看完这几篇参考文档,不然下面讲解一些对象或者函数会不理解。
深入进阶-从一次点击探寻Flutter事件分发原理 - 掘金
Flutter分享:Flutter事件分发原理 - 掘金
8.3 Flutter事件机制 | 《Flutter实战·第二版》
8.4 手势原理与手势冲突 | 《Flutter实战·第二版》
Flutter事件传递简单概述
重要对象介绍
HitTestEntry:可以把它看成视图中的 手势监听组件,主要信息都在 target 属性中。
HitTestResult:翻译为 命中测试结果,重点是它的 _path 集合保存着 HitTestEntry 对象;
重要函数介绍
hitTest(result,position) 翻译为 命中测试 ,手势监听组件 内部会调用的方法,如果返回true,会将当前 手势监听组件 也就是 HitTestEntry 加入 HitTestResult._path 集合中,这只是默认规则,可以手动添加。
核心代码:result.add(BoxHitTestEntry(this, position)),将 HitTestEntry (手势监听组件) 加入 HitTestResult._path 集合中;
核心代码:`result.add(BoxHitTestEntry(this, position))`:将 `HitTestEntry` **(手势监听组件)** 加入 `HitTestResult._path` 集合中;
还有查找 监听组件的顺序,是由深到浅的查找,比如 父子结构查找顺序:子孙手势组件、子手势组件、父手势组件,其他传统布局查找顺序:兄弟手势组件03、兄弟手势组件02、兄弟手势组件01。
那这个 hitTest函数的 布尔值是不是没用了?当然有用,后面会讲解,先忽略;
最开始执行的是 renderView.hitTest(result, position: position) ,renderView 表示 渲染树的根节点;
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {
bool hitTest(HitTestResult result, { required Offset position }) {
// 这部分逻辑是父子结构的组件,才走的
if (child != null) {
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
}
// 你手指触摸位置的那个 手势监听组件,加入 HitTestResult._path 集合中
result.add(HitTestEntry(this));
return true;
}
}
abstract class RenderBox extends RenderObject {
// 父子结构的组件,走到这
bool hitTest(BoxHitTestResult result, { required Offset position }) {
... ...
if (_size!.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
}
常用的手势监听组件
Listener组件
它只监听最原始的几种事件,down ==> move ==> ... ==> move ==> up ==> cancel;
比如 第一次将手指放在屏幕上 触发 Down 事件,手指没有离开屏幕前,手指位置发生改变 触发 Move 事件,每次位置改变都会触发一次 Move 事件,手指离开屏幕时触发 Up事件,紧接着 触发 Cancel事件;
常用的一些手势,比如 单击、双击、长按 等等,它都识别不了,也不负责处理事件冲突;
Listener(
onPointerDown: (event) {
debugPrint('onPointerDown');
},
child: Container(
width: 100,
height: 100,
color: Colors.primaries[10],
),
)
GestureDetector
对Listener的封装后的产物,内部加了很多 GestureRecognizer (手势识别器),每个识别器都代表一种手势监听,比如监听 单击、双击、长按、缩放 等等手势,以及可以通过自定义手势识别器解决事件冲突,所以一般都用它;
GestureDetector(
onTap: () {
debugPrint('onTap');
},
child: Container(
width: 100,
height: 100,
color: Colors.primaries[10],
),
)
class GestureDetector extends StatelessWidget {
... ...
@override
Widget build(BuildContext context) {
... ...
// TapGestureRecognizer 单击手势识别器
gestures[TapGestureRecognizer] = ... ...
// DoubleTapGestureRecognizer 双击手势识别器
gestures[DoubleTapGestureRecognizer] = ... ...
... ...
return RawGestureDetector(
... ...
);
}
}
class RawGestureDetector extends StatefulWidget {
... ...
@override
RawGestureDetectorState createState() => RawGestureDetectorState();
}
class RawGestureDetectorState extends State<RawGestureDetector> {
... ...
@override
Widget build(BuildContext context) {
Widget result = Listener( // 原始手势监听器
... ...
);
... ...
return result;
}
... ...
}
InkWell
对GestureDetector的封装,加了点击时出现水波纹效果,我项目里基本不用这东西。
注意:它这个水波纹效果,实现位置是在 Child 下面,所以Child 颜色要为透明,不然看不见;
一般是通过 Material 组件设置背景色,来解决这个问题。
Material(
color: Colors.greenAccent, // 设置背景色
child: InkWell(
onTap: () {
debugPrint('onTap');
},
child: Container(
width: 100,
height: 100,
),
),
),
class InkWell extends InkResponse {
... ...
}
class InkResponse extends StatelessWidget {
... ...
@override
Widget build(BuildContext context) {
... ...
return _InkResponseStateWidget(
... ...
);
}
... ...
}
class _InkResponseStateWidget extends StatefulWidget {
... ...
@override
_InkResponseState createState() => _InkResponseState();
... ...
}
class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKeepAliveClientMixin<_InkResponseStateWidget> implements _ParentInkResponseState {
... ...
@override
Widget build(BuildContext context) {
... ...
return _ParentInkResponseProvider(
... ...
child: GestureDetector( // 手势监听器
... ...
),
),
);
}
... ...
}
事件传递过程
这个过程是我根据断点调试顺序构思的,如有错误,还请评论区留言,共勉。
默认传递过程
使用HitTestBehavior的传递过程
HitTestBehavior
翻译 命中测试行为,它不是一个对象,只是一个概念,让我们自己写 命中测试 逻辑,通过以下两个对象 实现。
RenderProxyBox:它是RenderObject的子类,可以重写 hitTest 命中测试函数,从而修改事件传递过程,RenderObject 属于 渲染树,无法直接在 Widget树 中使用,需要包一层 SingleChildRenderObjectWidget。
SingleChildRenderObjectWidget:用来将 RenderObject 类型的组件,转换成 RenderObjectWidget,让其 可以在 Widget树中 使用;
会涉及到两个知识点:
- 事件中断机制;
- 还有 hitTest 命中测试函数 返回布尔值 有什么用;
我都写在代码注释里;
如果你想自定义手势,建议去研究 GestureDetector 里的 GestureRecognizer (手势识别器),因为它用的最多,有的组件 甚至提供了 GestureRecognizer类型参数。
class MyListener extends SingleChildRenderObjectWidget {
MyListener(
{super.key,
this.downEventListener,
this.hitTestBehavior = MyHitTestBehavior.normal,
super.child});
PointerDownEventListener? downEventListener;
MyHitTestBehavior hitTestBehavior;
@override
RenderObject createRenderObject(BuildContext context) {
return MyRenderListener(
downEventListener: downEventListener, hitTestBehavior: hitTestBehavior);
}
@override
void updateRenderObject(
BuildContext context, covariant MyRenderListener renderObject) {
renderObject.downEventListener = downEventListener;
renderObject.hitTestBehavior = hitTestBehavior;
}
}
class MyRenderListener extends MyRenderHitTestBehavior {
MyRenderListener({this.downEventListener, super.hitTestBehavior});
PointerDownEventListener? downEventListener;
@override
void handleEvent(PointerEvent event, covariant HitTestEntry<HitTestTarget> entry) {
if (event is PointerDownEvent) {
return downEventListener?.call(event);
}
}
}
abstract class MyRenderHitTestBehavior extends RenderProxyBox {
MyRenderHitTestBehavior({this.hitTestBehavior = MyHitTestBehavior.normal});
MyHitTestBehavior hitTestBehavior;
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if(hitTestBehavior == MyHitTestBehavior.normal) { // 默认
return super.hitTest(result, position: position);
}
if(hitTestBehavior == MyHitTestBehavior.ignore) {
return false; // 强制命中测试失败
}
// 下面两个判断,区别在于 返回布尔值不一样
// 同一容器内的 兄弟级别事件监听组件,只要有一个返回true,
// 其他的都会返回false,这叫 事件中断机制,触发了这个机制,
// 这些返回false的,将不参与 事件命中测试,即使加入了 HitTestResult.path 集合 也没用
// 因为这些 事件监听组件的 handleEvent 没有触发
// 注意:是触发了 中断机制 之后,这些返回false的 事件监听组件 才不参与 事件命中测试
// 不是因为返回值是false,就不参与 事件命中测试,跟 false 没啥关系
// 不触发 中断机制 的方法
// 全部返回 false,这样只要在 HitTestResult.path 里的事件监听组件,都会被 分发事件
if(hitTestBehavior == MyHitTestBehavior.opaque) {
if(size.contains(position)) { // 点击的坐标,是否在 事件监听组件 范围内
result.add(BoxHitTestEntry(this, position));
return true; // 强制命中测试成功,会触发中断机制
}
}
if (hitTestBehavior == MyHitTestBehavior.avoidInterruptions) {
// 注意:这里我没有使用这个 范围判断,触发范围会变成 它父级组件 范围
// if(size.contains(position))
result.add(BoxHitTestEntry(this, position));
return false; // 强制命中测试失败,不会触发中断机制
}
return false;
}
@override
bool hitTestSelf(Offset position) => super.hitTestSelf(position);
// hitTestSelf函数 是父子结构组件 的判断条件 之一,你点开 super.hitTest(result, position: position);源码
// 父子结构组件
// return Listener( // 父组件
// ... ...
// child: Container(
// ... ...
// child: Listener( // 子组件
// ... ...
// child: Container(
// ... ...
// ),
// ),
// ),
// );
// super.hitTest(result, position: position); 源码:
// 重点代码:如果子组件全都 命中测试失败,那就判断 hitTestSelf函数的 返回值
// if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
// ... ...
// }
// bool hitTest(BoxHitTestResult result, { required Offset position }) {
// ... ...
// if (_size!.contains(position)) {
// if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
// result.add(BoxHitTestEntry(this, position));
// return true;
// }
// }
// return false;
// }
}
enum MyHitTestBehavior {
ignore, // 不参与 命中测试
opaque, // 强制命中测试成功
avoidInterruptions, // 避免触发中断机制
normal // 默认
}
使用 MyListener
Widget box(int index, double size) {
return MyListener(
// hitTestBehavior: MyHitTestBehavior.ignore, // 事件拦截
hitTestBehavior: MyHitTestBehavior.avoidInterruptions, // 所有兄弟节点都会被分发事件
downEventListener: (event) {
debugPrint('index:$index');
},
child: Container(
width: size,
height: size,
color: Colors.primaries[index],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
color: Colors.greenAccent,
width: 150,
height: 400,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
box(1, 100),
box(2, 100),
box(3, 100),
box(4, 100),
],
),
),
],
)),
);
}
事件冒泡
事件冒泡的产生原因
在父子结构组件中,父组件会先调用 hitTestChildren 方法,最后调用自身的 hitTest方法;
父组件判断自身是否 命中测试 的条件:只要有一个子组件的 hitTest 方法 返回true,父组件 hitTest方法 也会返回true,导致它会执行handleEvent方法,递归这个过程,就会产生事件冒泡;
hitTestChildren(result, position):执行子组件的 hitTest 方法;
// 事件冒泡代码
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Listener(
onPointerDown: (event) {
debugPrint('Parent --- onPointerDown');
},
child: Container(
width: 300,
height: 300,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[10],
alignment: Alignment.center,
child: Listener(
onPointerDown: (event) {
debugPrint('Child01 --- onPointerDown');
},
child: Container(
width: 200,
height: 200,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[8],
alignment: Alignment.center,
child: Listener(
onPointerDown: (event) {
debugPrint('Child02 --- onPointerDown');
},
child: Container(
width: 100,
height: 100,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[11],
),
)
)
),
),
),
],
)
解决方式一:通过变量判断
// 解决方式一:通过变量判断
Builder(
builder: (context) {
bool childEvent = false;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Listener(
onPointerDown: (event) {
if(!childEvent) {
debugPrint('Parent --- onPointerDown');
}
childEvent = false;
},
child: Container(
width: 300,
height: 300,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[10],
alignment: Alignment.center,
child: Listener(
onPointerDown: (event) {
if(!childEvent) {
debugPrint('Child01 --- onPointerDown');
childEvent = true;
}
},
child: Container(
width: 200,
height: 200,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[8],
alignment: Alignment.center,
child: Listener(
onPointerDown: (event) {
debugPrint('Child02 --- onPointerDown');
childEvent = true;
},
child: Container(
width: 100,
height: 100,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[11],
),
)
)
),
),
),
],
);
}
),
解决方式二:使用GestureDetector
// 使用GestureDetector解决
// 注意一:
// 有参数的事件回调,还是会触发冒泡,比如onTapDown(details),以此类推
// onTap():可以防止冒泡,onTapDown(details)不可以;
// onDoubleTap():可以防止冒泡,onDoubleTapDown(details)不可以;
//
// 注意二:而且它俩都是up事件,手指离开屏幕时才会触发
// ... ...
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
debugPrint('Parent --- onPointerDown');
},
child: Container(
width: 300,
height: 300,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[10],
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
debugPrint('Child01 --- onPointerDown');
},
child: Container(
width: 200,
height: 200,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[8],
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
debugPrint('Child02 --- onPointerDown');
},
child: Container(
width: 100,
height: 100,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[11],
),
)
)
),
),
),
],
),
事件穿透
在叠加布局中,两个组件是位置相同,相互覆盖,且两个都有事件,如何忽略盖在上面的组件事件,只触发底层的组件事件,这种场景出现的很少;
事件穿透应用场景:在叠加布局中,两个组件是位置相同,相互覆盖,且两个都注册了事件监听器,如何忽略盖在上面的组件事件,只触发底层组件的事件;
这里介绍一下 IgnorePointer 和 AbsorbPointer 组件,它们的原理就是让这些组件不参与命中测试,从而做到事件拦截;
- IgnorePointer组件:包裹的组件,以及子组件、子孙后代组件,都不参与命中测试;
- AbsorbPointer组件:包裹组件的 子组件、子孙后代组件 不参与命中测试,但不包括自身,点击子组件区域,还是会触发自身事件;
它俩都有一个是否启用的布尔值参数,默认为true,表示启用,可以通过变量动态操控;
使用IgnorePointer,包裹的组件事件被完全拦截,可以做到事件穿透的效果,反之AbsorbPointer不可以;
// 在叠加布局中使用
Stack(
alignment: Alignment.center,
children: [
Listener(
onPointerDown: (event) {
debugPrint('Child01 --- onPointerDown');
},
child: Container(
width: 300,
height: 300,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[10],
),
),
// Listener(
// onPointerDown: (event) {
// debugPrint('Child02 --- onPointerDown');
// },
// child: IgnorePointer(
// child: Container(
// width: 200,
// height: 200,
// margin: const EdgeInsets.only(bottom: 12),
// color: Colors.primaries[8],
// ),
// )
// ),
// 或者这样写 都可以
// 拦截当前组件事件,但同一位置的底层组件,会被触发,相当于穿透了
IgnorePointer(
child: Listener(
onPointerDown: (event) {
debugPrint('Child02 --- onPointerDown');
},
child: Container(
width: 200,
height: 200,
margin: const EdgeInsets.only(bottom: 12),
color: Colors.primaries[8],
)
),
),
// 拦截当前组件事件,但同一位置的底层组件无法触发,无法穿透
// AbsorbPointer(
// child: Listener(
// onPointerDown: (event) {
// debugPrint('Child02 --- onPointerDown');
// },
// child: Container(
// width: 200,
// height: 200,
// margin: const EdgeInsets.only(bottom: 12),
// color: Colors.primaries[8],
// )
// ),
// ),
],
),
事件竞争
- 当用户触摸屏幕时,可能同时触发好几种事件,这时候需要处理 事件冲突,确定哪一种 手势操作,Flutter提供了GestureArenaManager(手势竞技场)对象,将每一个手势当作一个竞选者,进行了筛选;
GestureArenaManager:官方视频:https://www.youtube.com/watch?v=Q85LBtBdi0U&t=469s
每个手势都有自己的判定条件,且每次竞争,只能有一个胜利者,举例:
- 短按:手指按下 200毫秒
- 长按:手指按下 500毫秒
- ... ...
API 过时
以后要是找不到 hitTest 函数 就找 hitTestInView 函数。
官方文档
gestures library - Dart API