Flutter(八)事件处理与通知

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;@overrideWidget 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!"; //保存事件名@overrideWidget 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;//距左边的偏移@overrideWidget 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;@overrideWidget 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; //通过修改图片宽度来达到缩放效果@overrideWidget 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; //变色开关@overridevoid dispose() {//用到GestureRecognizer的话一定要调用其dispose方法释放资源_tapGestureRecognizer.dispose();super.dispose();}@overrideWidget 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.hqwc.cn/news/54197.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux从安装到实战 常用命令 Bash常用功能 用户和组管理

1.0初识Linux 1.1虚拟机介绍 1.2VMware Workstation虚拟化软件 下载CentOS; 1.3远程链接Linux系统 &FinalShell 链接finalshell半天没连接进去 他说ip adress 看IP地址是在虚拟机上 win11主机是 终端输入&#xff1a; ifconfig VMware虚拟机的设置 & ssh连接_snge…

Spring Cloud +UniApp 智慧工地云平台源码,智能监控和AI分析系统,危大工程管理、视频监控管理、项目人员管理、绿色施工管理

一套智慧工地云平台源码&#xff0c;PC管理端APP端平板端可视化数据大屏端源码 智慧工地可视化系统利用物联网、人工智能、云计算、大数据、移动互联网等新一代信息技术&#xff0c;通过工地中台、三维建模服务、视频AI分析服务等技术支撑&#xff0c;实现智慧工地高精度动态仿…

31 对集合中的字符串,按照长度降序排列

思路&#xff1a;使用集合的sort方法&#xff0c;新建一个Comparator接口&#xff0c;泛型是<String>&#xff0c;重写里面的compare方法。 package jiang.com; import java.util.Arrays; import java.util.Comparator; import java.util.List;public class Practice4 {…

单例模式和工厂模式

目录 今日良言&#xff1a;关关难过关关过&#xff0c;步步难行步步行 一、单例模式 1.饿汉模式 2.懒汉模式 二、工厂模式 今日良言&#xff1a;关关难过关关过&#xff0c;步步难行步步行 一、单例模式 首先来解释一下&#xff0c;什么是单例模式。 单例模式也就是单个…

Kubernetes架构和工作流程

目录 一、kubernetes简介 1.k8s的由来 2.为什么用 k8s &#xff1f; 3.k8s主要功能 二、k8s集群架构与组件 1.Master 组件 1.1Kube-apiserver 1.2Kube-controller-manager 1.3Kube-scheduler 2.Node组件 2.1Kubelet 2.2Kube-Proxy 2.3docker 或 rocket 3.配置存储中…

TCP的三次握手和四次挥手······详解

1、三次握手 三次握手是建立连接的过程 如图大致为三次握手的流程图&#xff1a; 当客户端对服务端发起连接时&#xff0c;会先发一个包连接请求数据&#xff0c;去询问能否建立连接&#xff0c;该数据包称为 “SYN”包 然后&#xff0c;如果对方同意连接&#xff0c;那么…

Java实现数据库表中的七种连接【Mysql】

Java实现数据库表中的七种连接【Mysql】 前言版权推荐Java实现数据库表中的七种连接左外连接右外连接其他连接 附录七种连接SQL测试Java测试转换方法类 Cla1类 Cla2类Cla3 最后 前言 2023-8-4 16:51:42 以下内容源自《【Mysql】》 仅供学习交流使用 版权 禁止其他平台发布时…

macOS下Django环境搭建

1. macOS升级pip /Library/Developer/CommandLineTools/usr/bin/python3 -m pip install --upgrade pip 2. 卸载Python3.9.5版本 $ sudo rm -rf /usr/local/bin/python3 $ sudo rm -rf /usr/local/bin/pip3 $ sudo rm -rf /Library/Frameworks/Python.framework 3. 安装P…

笔记本更换固态硬盘和清灰更换硅脂经历分享

本文仅供参考。 目录 购买 固态硬盘和固态硬盘盒 拆机工具 系统迁移 拆机 螺丝花了不要急 换硬盘 清灰 换导热硅脂 处理分区 购买 固态硬盘和固态硬盘盒 自己选购硬盘的时候一定要和自己电脑硬盘型号匹配。至于如何查看自己电脑硬盘型号&#xff0c;这个可以去自己…

Mac应用程序无法打开或文件损坏的处理方法

1. 打开系统偏好设置界面&#xff0c;进入安全性与隐私&#xff0c;如下图所示&#xff1a; 2. 点按左下角的锁头图标&#xff0c;解锁更改权限 3. 将允许从以下位置下载的应用&#xff0c;更改为 “ 任何来源 ” &#xff0c;然后再打开应用即可 有朋友就说了&#xff0c;我这…

一、ADAS技术概述

根据《国家车联网产业标准体系建设指南》对智能网联汽车定义&#xff1a;智能网联汽车是指搭载先进的车载传感器、控制器、执行器等装置&#xff0c;并融合现代通信与网络技术&#xff0c;实现车与X&#xff08;人、车、路、云端等&#xff09;智能信息交换、共享&#xff0c;具…

css在线代码生成器

这里收集了许多有意思的css效果在线代码生成器适合每一位前端开发者 布局&#xff0c;效果类&#xff1a; 网格生成器https://cssgrid-generator.netlify.app/ CSS Grid Generator可帮助开发人员使用CSS Grid创建复杂的网格布局。网格布局是创建Web页面的灵活和响应式设计的强…