之前的文章提到过,在 Flutter 开发中,一切皆是组件。因此,事件监听Listener也是一个组件。
原始指针事件
在移动端,各个平台或UI系统的原始指针事件模型基本都是一致,即:一次完整的事件分为三个阶段:手指按下、手指移动、和手指抬起。在 Flutter 中,我们使用 Listener
来处理原始的触摸事件,代码示例如下:
less
// 创建一个 Listener 组件,用于监听指针事件
Listener(
// 设置 Listener 组件的子组件
child: Container(
// 设置容器的宽度为 200 逻辑像素
width: 200,
// 设置容器的高度为 200 逻辑像素
height: 200,
// 设置容器的背景颜色为亮绿色
color: Colors.greenAccent,
// 设置容器的子组件为一个文本组件
child: Text(
// 显示父组件传递过来的 title 属性值
widget.title,
// 设置文本的样式,字体大小为 32 逻辑像素
style: TextStyle(fontSize: 32),
),
),
// 当指针按下时触发的回调函数
onPointerDown: (PointerDownEvent event) => print('onPointerDown'),
// 当指针移动时触发的回调函数
onPointerMove: (PointerMoveEvent event) => print('onPointerMove'),
// 当指针抬起时触发的回调函数
onPointerUp: (PointerUpEvent event) => print('onPointerUp'),
// 当指针事件被取消时触发的回调函数
onPointerCancel: (PointerCancelEvent event) => print('onPointerCancel'),
);
从上面示例代码可以看到,每个回调方法都会有一个event参数。虽然参数的类型各不相同,但是还有如下常用的属性:
- position:相对于全局坐标的偏移。
- delta:两次指针移动事件的距离。
- orientation:指针移动的方向,是一个角度值。
- pressure:按压力度,如果你的手机屏幕带压力传感器,那么可以结合这个属性实现很多非常有趣的动画效果。如果没有,该属性值始终为1。
忽略PointerEvent事件
假如我们不想让某个子树响应PointerEvent
的话,我们可以使用IgnorePointer
和AbsorbPointer
,这两个组件都能阻止子树接收指针事件。两者有以下区别。
IgnorePointer
:此节点与其子节点都将忽略点击事件,用ignoring参数区分是否忽略。AbsorbPointer
:此节点本身能够响应点击事件,但是它会阻止事件传递到子节点上。
IgnorePointer 示例如下:
less
class _PointerEventPageState extends State<PointerEventPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Listener(
child: IgnorePointer(
ignoring: true,
child: Listener(
child: Container(
width: 200,
height: 200,
color: Colors.greenAccent,
),
onPointerDown: (PointerDownEvent event)=>print("Listener2"),
),
),
onPointerDown: (PointerDownEvent event)=>print("Listener1"),
),
),
);
}
}
AbsorbPointer示例如下
less
class _PointerEventPageState extends State<PointerEventPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Listener(
child: AbsorbPointer(
child: Listener(
child: Container(
width: 200,
height: 200,
color: Colors.greenAccent,
),
onPointerDown: (PointerDownEvent event)=>print("Listener2"),
),
),
onPointerDown: (PointerDownEvent event)=>print("Listener1"),
),
),
);
}
}
GestureDetector
虽然原始指针事件处理普通的点击事件非常方便,但是App上的手势操作千变万化,这个时候就需要更强大的手势处理机制。
GestureDetector
是一个用于手势识别的功能性组件,我们通过它可以来识别各种手势,比如放大、缩小、双击等操作手势。GestureDetector 内部封装了 Listener,用以识别语义化的手势。代码示例如下:
less
class _GestureTestState extends State<GestureTest> {
String _operation = "No Gesture detected!"; //保存事件名
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 200.0,
height: 100.0,
child: Text(
_operation,
style: TextStyle(color: Colors.white),
),
),
onTap: () => updateText("Tap"), //点击
onDoubleTap: () => updateText("DoubleTap"), //双击
onLongPress: () => updateText("LongPress"), //长按
),
);
}
void updateText(String text) {
//更新显示的事件名
setState(() {
_operation = text;
});
}
}
GestureDetector 组件的常用属性如下图所示:
GestureRecognizer
GestureRecognizer
的作用是通过Listener
来将原始指针事件转换为语义手势,一种手势的识别器对应一个GestureRecognizer
的子类。GestureDetector
内部就是使用一个或多个GestureRecognizer
来实现识别各种手势的功能的。
这里以给一段富文本(RichText
)的不同部分分别添加点击事件处理器为例,代码示例如下:
scala
import 'package:flutter/gestures.dart';
class _GestureRecognizer extends StatefulWidget {
const _GestureRecognizer({Key? key}) : super(key: key);
@override
_GestureRecognizerState createState() => _GestureRecognizerState();
}
class _GestureRecognizerState extends State<_GestureRecognizer> {
TapGestureRecognizer _tapGestureRecognizer = TapGestureRecognizer();
bool _toggle = false; //变色开关
@override
void dispose() {
//用到GestureRecognizer的话一定要调用其dispose方法释放资源
_tapGestureRecognizer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Text.rich(
TextSpan(
children: [
TextSpan(text: "你好世界"),
TextSpan(
text: "点我变色",
style: TextStyle(
fontSize: 30.0,
color: _toggle ? Colors.blue : Colors.red,
),
recognizer: _tapGestureRecognizer
..onTap = () {
setState(() {
_toggle = !_toggle;
});
},
),
TextSpan(text: "你好世界"),
],
),
),
);
}
}
注意:使用
GestureRecognizer
后一定要调用其dispose()
方法来释放资源(主要是取消内部的计时器)。
手势冲突
当手势发生冲突时,flutter 引入了手势竞技场(Gesture Arena)来解决这个问题。原理是每一个手势识别器(GestureRecognizer
)都是一个"竞争者"(GestureArenaMember
),当发生指针事件时,他们都要在"竞技场"去竞争本次事件的处理权,默认情况最终只有一个"竞争者"会胜出(win)。
比如当一个组件同时监听水平和垂直方向的拖动手势时,我们斜着拖动时哪个方向的拖动手势回调会被触发?实际上取决于第一次移动时两个轴上的位移分量,哪个轴的大,哪个轴在本次滑动事件竞争中就胜出。代码如下所示:
less
class GestureArenaPage extends StatefulWidget {
GestureArenaPage({Key key, this.title}) : super(key: key);
final String title;
@override
_GestureArenaState createState() => _GestureArenaState();
}
class _GestureArenaState extends State<GestureArenaPage> {
double _left=0.0;
double _top=0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(
children: <Widget>[
Positioned(
left: _left,
top: _top,
child: GestureDetector(
child: OutlineButton(
child: Text('我是一个大按钮'),
),
onHorizontalDragUpdate: (DragUpdateDetails e){
setState(() {
_left+=e.delta.dx;
print('水平事件胜出');
});
},
onVerticalDragUpdate: (DragUpdateDetails e){
setState(() {
_top+=e.delta.dy;
print('垂直事件胜出');
});
},
onHorizontalDragEnd: (e){
print('水平移动结束');
},
onVerticalDragEnd: (e){
print('垂直移动结束');
},
onTapDown: (e){
print('按下');
},
onTapUp: (e){
print('抬起');
},
),
),
],
)
);
}
}
但是如果我们既想监听拖动的手势,也想监听手指按下抬起的手势,就无法实现。因为手势竞争最终只有一个胜出者。这时有两种方案解决:
- 使用 Listener。手势是对原始指针的语义化的识别,手势冲突只是手势级别的,也就是说只会在组件树中的多个 GestureDetector 之间才有冲突的场景,如果压根就没有使用 GestureDetector 则不存在所谓的冲突,因为每一个节点都能收到事件,这相当于跳出了手势识别那套规则。
- 自定义手势手势识别器( Recognizer)。
事件分发机制
在 Flutter 中,事件处理流程分为如下几步:
- 对渲染对象进行命中测试:当手指按下时,按照深度优先遍历当前渲染树(render object tree),对每一个渲染对象进行命中测试(hit test)。如果测试通过,则该对象会被添加到 HitTestResult 列表当中
- 事件分发:当命中测试完成后,遍历 HitTestResult 列表,并调用每一个渲染对象的 handleEvent 方法
- 事件清理:当手指抬( PointerUpEvent )起或事件取消时(PointerCancelEvent),会先对相应的事件进行分发,分发完毕后会清空 HitTestResult 列表。
注意:一旦有一个子节点的 hitTest 返回了 true,就会终止遍历,后续子节点将没有机会参与命中测试。
事件总线
在 App 中,我们经常会需要一个广播机制,用以跨页面事件通知,比如一个需要登录的 App 中,页面会关注用户登录或注销事件,来进行一些状态更新。我们可以通过事件总线来实现这个功能,代码如下所示:
scss
//订阅者回调签名
typedef void EventCallback(arg);
class EventBus {
//私有构造函数
EventBus._internal();
//保存单例
static EventBus _singleton = EventBus._internal();
//工厂构造函数
factory EventBus()=> _singleton;
//保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列
final _emap = Map<Object, List<EventCallback>?>();
//添加订阅者
void on(eventName, EventCallback f) {
_emap[eventName] ??= <EventCallback>[];
_emap[eventName]!.add(f);
}
//移除订阅者
void off(eventName, [EventCallback? f]) {
var list = _emap[eventName];
if (eventName == null || list == null) return;
if (f == null) {
_emap[eventName] = null;
} else {
list.remove(f);
}
}
//触发事件,事件触发后该事件所有订阅者会被调用
void emit(eventName, [arg]) {
var list = _emap[eventName];
if (list == null) return;
int len = list.length - 1;
//反向遍历,防止订阅者在回调中移除自身带来的下标错位
for (var i = len; i > -1; --i) {
list[i](arg);
}
}
}
//定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus
var bus = EventBus();
//页面A中
...
//监听登录事件
bus.on("login", (arg) {
// do something
});
//登录页B中
...
//登录成功后触发登录事件,页面A中订阅者会被调用
bus.emit("login", userInfo);
事件总线通常用于组件之间状态共享,但关于组件之间状态共享也有一些专门的包如redux、mobx以及前面介绍过的Provider。对于一些简单的应用,事件总线是足以满足业务需求的,如果你决定使用状态管理包的话,一定要想清楚您的 App 是否真的有必要使用它,防止"化简为繁"、过度设计。
通知 Notification
通知(Notification)是Flutter中一个重要的机制,在widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过NotificationListener
来监听通知。Flutter中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)
通知冒泡和用户触摸事件冒泡是相似的,但有一点不同:通知冒泡可以中止,但用户触摸事件不行。
监听通知
使用 NotificationListener 监听 ListView 的滑动通知。
php
NotificationListener(
onNotification: (notification){
switch (notification.runtimeType){
case ScrollStartNotification: print("开始滚动"); break;
case ScrollUpdateNotification: print("正在滚动"); break;
case ScrollEndNotification: print("滚动停止"); break;
case OverscrollNotification: print("滚动到边界"); break;
}
},
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
);
自定义通知
scala
// 定义一个通知类,要继承自Notification类
class MyNotification extends Notification {
MyNotification(this.msg);
final String msg;
}
class NotificationRoute extends StatefulWidget {
@override
NotificationRouteState createState() {
return NotificationRouteState();
}
}
class NotificationRouteState extends State<NotificationRoute> {
String _msg="";
@override
Widget build(BuildContext context) {
//监听通知
return NotificationListener<MyNotification>(
onNotification: (notification) {
setState(() {
_msg+=notification.msg+" ";
});
return true;
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// ElevatedButton(
// onPressed: () => MyNotification("Hi").dispatch(context),
// child: Text("Send Notification"),
// ),
Builder(
builder: (context) {
return ElevatedButton(
//按钮点击时分发通知
onPressed: () => MyNotification("Hi").dispatch(context),
child: Text("Send Notification"),
);
},
),
Text(_msg)
],
),
),
);
}
}
阻止通知冒泡
scala
class NotificationRouteState extends State<NotificationRoute> {
String _msg="";
@override
Widget build(BuildContext context) {
//监听通知
return NotificationListener<MyNotification>(
onNotification: (notification){
print(notification.msg); //打印通知
return false;
},
child: NotificationListener<MyNotification>(
onNotification: (notification) {
setState(() {
_msg+=notification.msg+" ";
});
// 回调返回了false,表示不阻止冒泡
return false;
},
child: ...//省略重复代码
),
);
}
}