Flutter(八)事件处理与通知

news2024/12/28 5:10:36

1.原始指针事件处理

一次完整的事件分为三个阶段:手指按下、手指移动、和手指抬起,而更高级别的手势(如点击、双击、拖动等)都是基于这些原始事件的。

Listener 组件

Flutter中可以使用Listener来监听原始触摸事件

Listener({
  Key key,
  this.onPointerDown, //手指按下回调
  this.onPointerMove, //手指移动回调
  this.onPointerUp,//手指抬起回调
  this.onPointerCancel,//触摸事件取消回调
  this.behavior = HitTestBehavior.deferToChild, //先忽略此参数,后面小节会专门介绍
  Widget child
})

示例:手指在一个容器上移动时查看手指相对于容器的位置

class _PointerMoveIndicatorState extends State<PointerMoveIndicator> {
  PointerEvent? _event;

  @override
  Widget build(BuildContext context) {
    return Listener(
      child: Container(
        alignment: Alignment.center,
        color: Colors.blue,
        width: 300.0,
        height: 150.0,
        child: Text(
          '${_event?.localPosition ?? ''}',
          style: TextStyle(color: Colors.white),
        ),
      ),
      onPointerDown: (PointerDownEvent event) => setState(() => _event = event),
      onPointerMove: (PointerMoveEvent event) => setState(() => _event = event),
      onPointerUp: (PointerUpEvent event) => setState(() => _event = event),
    );
  }
}

PointerEvent常用属性:

  • position:它是指针相对于当对于全局坐标的偏移。
  • localPosition: 它是指针相对于当对于本身布局坐标的偏移。
  • delta:两次指针移动事件(PointerMoveEvent)的距离。
  • pressure:按压力度,如果手机屏幕支持压力传感器(如iPhone的3D Touch),此属性会更有意义,如果手机不支持,则始终为1。
  • orientation:指针移动方向,是一个角度值。

忽略指针事件

不想让某个子树响应PointerEvent的话,我们可以使用IgnorePointer和AbsorbPointer,AbsorbPointer本身是可以接收指针事件的(但其子树不行),而IgnorePointer不可以

Listener(
  child: AbsorbPointer(
    child: Listener(
      child: Container(
        color: Colors.red,
        width: 200.0,
        height: 100.0,
      ),
      onPointerDown: (event)=>print("in"),
    ),
  ),
  onPointerDown: (event)=>print("up"),
)

点击Container时,由于它在AbsorbPointer的子树上,所以不会响应指针事件,所以日志不会输出"in",
但AbsorbPointer本身是可以接收指针事件的,所以会输出"up"。

如果将AbsorbPointer换成IgnorePointer,那么两个都不会输出。

2.手势识别

GestureDetector和GestureRecognizer

点击、双击、长按

通过GestureDetector对Container进行手势识别,触发相应事件后,在Container上显示事件名

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;
    });
  }
}

拖动、滑动

拖动圆形字母A的示例:

class _Drag extends StatefulWidget {
  @override
  _DragState createState() => _DragState();
}

class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
  double _top = 0.0; //距顶部的偏移
  double _left = 0.0;//距左边的偏移

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //手指按下时会触发此回调
            onPanDown: (DragDownDetails e) {
              //打印手指按下的位置(相对于屏幕)
              print("用户手指按下:${e.globalPosition}");
            },
            //手指滑动时会触发此回调
            onPanUpdate: (DragUpdateDetails e) {
              //用户手指滑动时,更新偏移,重新构建
              setState(() {
                _left += e.delta.dx;
                _top += e.delta.dy;
              });
            },
            onPanEnd: (DragEndDetails e){
              //打印滑动结束时在x、y轴上的速度
              print(e.velocity);
            },
          ),
        )
      ],
    );
  }
}
I/flutter ( 8513): 用户手指按下:Offset(26.3, 101.8)
I/flutter ( 8513): Velocity(235.5, 125.8)
  • DragDownDetails.globalPosition:当用户按下时,此属性为用户按下的位置相对于屏幕(而非父组件)原点(左上角)的偏移。
  • DragUpdateDetails.delta:当用户在屏幕上滑动时,会触发多次Update事件,delta指一次Update事件的滑动的偏移量。
  • DragEndDetails.velocity:该属性代表用户抬起手指时的滑动速度(包含x、y两个轴的)

单一方向拖动

GestureDetector可以只识别特定方向的手势事件,我们将上面的例子改为只能沿垂直方向拖动:

class _DragVertical extends StatefulWidget {
  @override
  _DragVerticalState createState() => _DragVerticalState();
}

class _DragVerticalState extends State<_DragVertical> {
  double _top = 0.0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //垂直方向拖动事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            },
          ),
        )
      ],
    );
  }
}

缩放

class _Scale extends StatefulWidget {
  const _Scale({Key? key}) : super(key: key);

  @override
  _ScaleState createState() => _ScaleState();
}

class _ScaleState extends State<_Scale> {
  double _width = 200.0; //通过修改图片宽度来达到缩放效果

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        //指定宽度,高度自适应
        child: Image.asset("./images/sea.png", width: _width),
        onScaleUpdate: (ScaleUpdateDetails details) {
          setState(() {
            //缩放倍数在0.8到10倍之间
            _width=200*details.scale.clamp(.8, 10.0);
          });
        },
      ),
    );
  }
}

GestureDetector内部是使用一个或多个GestureRecognizer来识别各种手势的,

而GestureRecognizer的作用就是通过Listener来将原始指针事件转换为语义手势,

GestureDetector直接可以接收一个子widget

示例:给一段富文本(RichText)的不同部分分别添加点击事件处理器,但是TextSpan并不是一个widget,这时我们不能用GestureDetector,但TextSpan有一个recognizer属性,它可以接收一个GestureRecognizer。

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: "你好世界"),
          ],
        ),
      ),
    );
  }
}

在这里插入图片描述

3.Flutter事件机制

事件分发过程很简单,即遍历HitTestResult,调用每一个节点的 handleEvent 方法,只需要重写 handleEvent 方法就可以处理事件了。

1.命中测试:当手指按下时,触发 PointerDownEvent 事件,按照深度优先遍历当前渲染(render object)树,对每一个渲染对象进行“命中测试”(hit test),如果命中测试通过,则该渲染对象会被添加到一个 HitTestResult 列表当中。
2.事件分发:命中测试完毕后,会遍历 HitTestResult 列表,调用每一个渲染对象的事件处理方法(handleEvent)来处理 PointerDownEvent 事件,该过程称为“事件分发”(event dispatch)。随后当手指移动时,便会分发 PointerMoveEvent 事件。
3.事件清理:当手指抬( PointerUpEvent )起或事件取消时(PointerCancelEvent),会先对相应的事件进行分发,分发完毕后会清空 HitTestResult 列表。

4.手势原理与手势冲突

手势发生变化时只需要在 pointerRouter中取出 GestureRecognizer 的 handleEvent 方法进行手势识别即可。一个手势只有一个手势识别器生效

1.每一个手势识别器(GestureRecognizer)都是一个“竞争者”(GestureArenaMember),当发生指针事件时,他们都要在“竞技场”去竞争本次事件的处理权,默认情况最终只有一个“竞争者”会胜出(win)。
2.GestureRecognizer 的 handleEvent 中会识别手势,如果手势发生了某个手势,竞争者可以宣布自己是否胜出,一旦有一个竞争者胜出,竞技场管理者(GestureArenaManager)就会通知其他竞争者失败。
3.胜出者的 acceptGesture 会被调用,其余的 rejectGesture 将会被调用。

如果对一个组件同时监听水平和垂直方向的拖动手势,当我们斜着拖动时哪个方向的拖动手势回调会被触发?实际上取决于第一次移动时两个轴上的位移分量,哪个轴的大,哪个轴在本次滑动事件竞争中就胜出

解决手势冲突

解决手势冲突的方法有两种:

1.使用 Listener。这相当于跳出了手势识别那套规则。
2.自定义手势手势识别器( Recognizer)

通过 Listener 解决手势冲突的原因是竞争只是针对手势的,而 Listener 是监听原始指针事件,原始指针事件并非语义话的手势,所以根本不会走手势竞争的逻辑,所以也就不会相互影响

Listener(  // 将 GestureDetector 换位 Listener 即可
  onPointerUp: (x) => print("2"),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.red,
    alignment: Alignment.center,
    child: GestureDetector(
      onTap: () => print("1"),
      child: Container(
        width: 50,
        height: 50,
        color: Colors.grey,
      ),
    ),
  ),
);

代码很简单,只需将 GestureDetector 换位 Listener 即可,可以两个都换,也可以只换一个。可以看见,通过Listener直接识别原始指针事件来解决冲突的方法很简单,因此,当遇到手势冲突时,我们应该优先考虑 Listener 。

定义手势识别器的方式比较麻烦,原理是当确定手势竞争胜出者时,会调用胜出者的acceptGesture 方法,表示“宣布成功”,然后会调用其他手势识别其的rejectGesture 方法,表示“宣布失败”。

既然如此,我们可以自定义手势识别器(Recognizer),然后去重写它的rejectGesture 方法:在里面调用acceptGesture 方法,这就相当于它失败是强制将它也变成竞争的成功者了,这样它的回调也就会执行。

5.事件总线

一个需要登录的 App 中,页面会关注用户登录或注销事件,来进行一些状态更新。这时候,一个事件总线便会非常有用

//订阅者回调签名
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);

Dart中实现单例模式的标准做法就是使用static变量+工厂构造函数的方式,这样就可以保证EventBus()始终返回都是同一个实例

事件总线通常用于组件之间状态共享,但关于组件之间状态共享也有一些专门的包如redux、mobx以及前面介绍过的Provider。对于一些简单的应用,事件总线是足以满足业务需求的

6. 通知 Notification

通知(Notification)是Flutter中一个重要的机制,在widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过NotificationListener来监听通知。Flutter中将这种由子向父的传递通知的机制称为通知冒泡

监听可滚动组件滚动通知的例子:

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"),);
    }
  ),
);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/836464.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Go语言开发者的Apache Arrow使用指南:读写Parquet文件

Apache Arrow是一种开放的、与语言无关的列式内存格式&#xff0c;在本系列文章[1]的前几篇中&#xff0c;我们都聚焦于内存表示[2]与内存操作[3]。 但对于一个数据库系统或大数据分析平台来说&#xff0c;数据不能也无法一直放在内存中&#xff0c;虽说目前内存很大也足够便宜…

FuncGPT竟然限免?!速来体验全球首个AIGF!

近日&#xff0c;飞算SoFlu软件机器人重磅推出全新功能——FuncGPT&#xff08;慧函数&#xff09;。FuncGPT&#xff08;慧函数&#xff09;是一款函数AI生产器&#xff0c;它能够根据用户的需求快速生成 Java 语言的函数代码。FuncGPT&#xff08;慧函数&#xff09;拥有强大…

行政资产管理信息系统

行政资产管理信息系统是通过专业设计开发的资产管理解决方案&#xff0c;旨在为企业建立和完善资产管理体系。该系统可以有效地控制资产的购买和应用&#xff0c;从而节省资金&#xff0c;完成资产的有效管理。   资产管理信息系统的核心功能是统一资产管理&#xff0c;可以…

java 版本企业招标投标管理系统源码+多个行业+tbms+及时准确+全程电子化

​ 功能描述 1、门户管理&#xff1a;所有用户可在门户页面查看所有的公告信息及相关的通知信息。主要板块包含&#xff1a;招标公告、非招标公告、系统通知、政策法规。 2、立项管理&#xff1a;企业用户可对需要采购的项目进行立项申请&#xff0c;并提交审批&#xff0c;查…

kernel pwn入门

Linux Kernel 介绍 Linux 内核是 Linux 操作系统的核心组件&#xff0c;它提供了操作系统的基本功能和服务。它是一个开源软件&#xff0c;由 Linus Torvalds 在 1991 年开始开发&#xff0c;并得到了全球广泛的贡献和支持。 Linux 内核的主要功能包括进程管理、内存管理、文…

2023-08-04 Untiy进阶 C#知识补充4——C#5主要功能与语法

文章目录 一、概述二、回顾——线程三、线程池四、Task 任务类五、同步和异步 ​ 注意&#xff1a;在此仅提及 Unity 开发中会用到的一些功能和特性&#xff0c;对于不适合在 Unity 中使用的内容会忽略。 一、概述 C# 5 调用方信息特性&#xff08;C# 进阶内容&#xff09;异步…

途乐证券|俄罗斯宣布9月削减石油出口量

当地时间周四&#xff0c;美股兜售潮仍在持续&#xff0c;三大股指连续第二个交易日团体收跌。到收盘&#xff0c;道指跌落0.19%&#xff0c;标普500指数跌落0.25%&#xff0c;纳指跌幅为0.10%。 美国ISM7月非制造业PMI下滑 数据面上&#xff0c;美国供应办理协会ISM周四发布的…

掌握好视频翻译软件的使用方法,帮你跨越语言障碍

嘿&#xff0c;翻译小达人们&#xff0c;你知道吗&#xff0c;当你看到一段充满神秘符号的英语视频&#xff0c;脑袋里冒出一大片问号的时候&#xff0c;别慌&#xff01;我们有比手动翻译更妙的解决办法——视频翻译。嗯&#xff0c;这货可不一般&#xff0c;它能帮你解读视频…

锂电设备振动监测:早发现问题,早预防故障

锂电设备在生产过程中的振动问题可能导致设备故障、损坏和生产线停机&#xff0c;对企业产生严重影响。为了确保锂电池生产的稳定性和可靠性&#xff0c;振动监测成为了关键一步。通过引入智能无线温振传感器及其监测分析软件&#xff0c;企业可以早发现问题、早预防故障&#…

[PaddlePaddle] [学习笔记] PaddlePaddle 官方文档 —— 使用Python和NumPy构建神经网络模型

1. 机器学习和深度学习综述 1.1 人工智能、机器学习、深度学习的关系 近些年人工智能、机器学习和深度学习的概念十分火热&#xff0c;但很多从业者却很难说清它们之间的关系&#xff0c;外行人更是雾里看花。在研究深度学习之前&#xff0c;先从三个概念的正本清源开始。概括…

节能延寿:ARM Cortex-M微控制器下的低功耗定时器应用

嵌入式系统的开发在现代科技中发挥着至关重要的作用。它们被广泛应用于从智能家居到工业自动化的各种领域。在本文中,我们将聚焦于使用ARM Cortex-M系列微控制器实现低功耗定时器的应用。我们将详细介绍在嵌入式系统中如何实现低功耗的定时器功能,并附上代码示例。 嵌入式系…

详聊API接口?淘宝API接口在ERP系统中扮演者什么角色?

什么是API&#xff1f; API全称应用程序编程接口&#xff08;Application Programming Interface&#xff09;&#xff0c;是一组用于访问某个软件或硬件的协议、规则和工具集合。电商API就是各大电商平台提供给开发者访问平台数据的接口。目前&#xff0c;主流电商平台如淘宝…

Ubuntu 22.04安装和使用ROS1可行吗

可行。 测试结果 ROS1可以一直使用下去的&#xff0c;这一点不用担心。Ubuntu会一直维护的。 简要介绍 Debian发行版^_^ AI&#xff1a;在Ubuntu 22.04上安装ROS1是可行的&#xff0c;但需要注意ROS1对Ubuntu的支持只到20.04。因此&#xff0c;如果要在22.04上安装ROS1&am…

Kubernetes v1.20 二进制部署

架构 k8s集群master01&#xff1a;192.168.80.101 kube-apiserver kube-controller-manager kube-scheduler etcd k8s集群master02&#xff1a;192.168.80.102 k8s集群node01&#xff1a;192.168.80.103 kubelet kube-proxy docker k8s集群node02&#xff1a;192.168.80…

问题解决和批判性思维是软件工程的重要核心

软件工程的重心在于问题解决和批判性思维&#xff08;合理设计和架构降低复杂度&#xff09;&#xff0c;而非仅局限于编程。 许多人误以为软件工程就只是编程&#xff0c;即用编程语言编写指令&#xff0c;让计算机按照这些指令行事。但实际上&#xff0c;软件工程的内涵远超…

Leetcode-每日一题【剑指 Offer 04. 二维数组中的查找】

题目 在一个 n * m 的二维数组中&#xff0c;每一行都按照从左到右 非递减 的顺序排序&#xff0c;每一列都按照从上到下 非递减 的顺序排序。请完成一个高效的函数&#xff0c;输入这样的一个二维数组和一个整数&#xff0c;判断数组中是否含有该整数。 示例: 现有矩阵 matri…

机器学习基础之《特征工程(3)—特征预处理》

一、什么是特征预处理 通过一些转换函数将特征数据转换成更加适合算法模型的特征数据过程 处理前&#xff0c;特征值是数值&#xff0c;处理后&#xff0c;进行了特征缩放

学习笔记|C251|STC32G单片机视频开发教程(冲哥)|第三集:开发环境搭建和程序下载

文章目录 1.STC-ISP软件的下载2.STC32手册下载3.PDF阅读器下载4.学会PDF阅读器查阅手册5.跟着手册搭建C251开发环境Tips:如何同时安装Keil的C51、C251和MDK 6.程序包的下载7.第一个工程的编译和下载 原作者/主讲人&#xff1a;冲哥 原始视频地址 1.STC-ISP软件的下载 STC-ISP …

在线会议的线下战场:补齐产业故事里的第一个短板

过去几年时间里&#xff0c;在线会议产品以其互联网的强产品体验和线上办公的必选项属性站在了TO B风口上&#xff0c;但在水温变化的如今&#xff0c;这个冷却的赛道在增长之外&#xff0c;更迎来了新的审视&#xff1a;即用类互联网的打法&#xff0c;能否彻底渗透TO B市场&a…

weui 去掉输入框的边框 每一项目的边框删除

问题&#xff1a; 边框是通过before添加&#xff0c;如下源码是&#xff1a; .weui_cell:before {content: " ";position: absolute;left: 15px;top: 0;width: 100%;height: 1px;border-color:rgb(255 111 119);color: #D9D9D9;-webkit-transform-origin:0 0;transf…