Flutter 教程(五)事件处理

之前的文章提到过,在 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的话,我们可以使用IgnorePointerAbsorbPointer,这两个组件都能阻止子树接收指针事件。两者有以下区别。

  • 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('抬起');
              },
            ),
          ),
        ],
      )
    );
  }
}

但是如果我们既想监听拖动的手势,也想监听手指按下抬起的手势,就无法实现。因为手势竞争最终只有一个胜出者。这时有两种方案解决:

  1. 使用 Listener。手势是对原始指针的语义化的识别,手势冲突只是手势级别的,也就是说只会在组件树中的多个 GestureDetector 之间才有冲突的场景,如果压根就没有使用 GestureDetector 则不存在所谓的冲突,因为每一个节点都能收到事件,这相当于跳出了手势识别那套规则。
  2. 自定义手势手势识别器( Recognizer)。

事件分发机制

在 Flutter 中,事件处理流程分为如下几步:

  1. 对渲染对象进行命中测试:当手指按下时,按照深度优先遍历当前渲染树(render object tree),对每一个渲染对象进行命中测试(hit test)。如果测试通过,则该对象会被添加到 HitTestResult 列表当中
  2. 事件分发:当命中测试完成后,遍历 HitTestResult 列表,并调用每一个渲染对象的 handleEvent 方法
  3. 事件清理:当手指抬( 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: ...//省略重复代码
      ),
    );
  }
}

参考

相关推荐
小墙程序员18 分钟前
Flutter 教程(九)权限
flutter
pengyu2 小时前
系统化掌握Dart网络编程之Dio(二):责任链模式篇
android·flutter·dart
侑柚酒2 小时前
一个例子直观的告诉你flutter中key的作用
flutter
pengyu4 小时前
系统化掌握Dart网络编程之Dio(二):配置管理篇
android·flutter·dart
蹲街式等待5 小时前
Flutter dart代码混淆与解混淆
flutter
唔668 小时前
flutter 曲线学习 使用第三方插件实现左右滑动
javascript·学习·flutter
harry235day9 小时前
Flutter getx 状态管理
flutter·前端框架
小墙程序员10 小时前
Flutter 教程(八)数据存储
flutter
pengyu10 小时前
系统化掌握Dart网络编程之Dio(一):筑基篇
android·flutter·dart
张风捷特烈11 小时前
Flutter 伪 3D 绘制#1 | 三维空间
android·flutter