您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
Flutter交互事件介绍
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Flutter交互事件介绍
自猿其说Tech
2022-05-16
IP归属:未知
14160浏览
前端
Flutter
### 1 引言 flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面,在响应用户交互行为flutter是有一套自己的事件传递机制的,那flutter是如何处理事件传递的,本文主要介绍如何监听和响应用户的手势操作。 ### 2 介绍 flutter的事件模型和移动端的事件模型基本一致的,也是一次完整的事件分为三个阶段:手指按下(down)、手指移动(move)、和手指抬起(cancel),flutter又做了一层细化的处理,手势操作在 flutter 中分为两类: - 原始的指针事件(Pointer Event),即原生开发中常见的触摸事件,表示屏幕上触摸(或鼠标、手写笔)行为触发的位移行为; - 手势识别(Gesture Detector),表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装。 ### 3 原始指针事件 在flutter中原始指针事件表示用户交互的原始触摸数据,如手指接触屏幕 PointerDownEvent、手指在屏幕上移动 PointerMoveEvent、手指抬起 PointerUpEvent,以及触摸取消 PointerCancelEvent,这与原生系统的底层触摸事件模型一致的。 在手指接触屏幕,触摸事件发起时,flutter会对应用程序执行命中测试(Hit Test),flutter 会确定手指与屏幕发生接触的位置上究竟有哪些组件,并将触摸事件交给最内层的组件去响应。与浏览器中的事件冒泡机制类似,事件会从这个最内层的组件开始,沿着组件树向根节点向上冒泡分发。不过 Flutter 无法像浏览器冒泡那样取消或者停止事件进一步分发,我们只能通过 hitTestBehavior 去调整组件在命中测试期内应该如何表现,比如把触摸事件交给子组件,或者交给其视图层级之下的组件去响应。 #### 3.1 Listener 关于组件层面flutter 提供了 Listener ,Listener可以监听其子 Widget 的原始指针事件, 例举Listener 的案例。利用 Listener 监听其内部 Down、Move 及 Up 事件: ``` return Listener( child: Container( color: Colors.red, width: 100, height: 100, ), onPointerDown: (event) => print("pointer down $event"),//手势按下回调 onPointerMove: (event) => print("pointer move $event"),//手势移动回调 onPointerUp: (event) => print("pointer up $event"),//手势抬起回调 ); ``` 模拟触摸点击、移动、抬起可以看到 Listener 监听到了一系列原始指针事件,并打印出了这些事件的位置信息: ``` I/flutter (19227): pointer down PointerDownEvent#90e54(position: Offset(56.4, 134.2)) I/flutter (19227): pointer move PointerMoveEvent#22c2b(position: Offset(56.7, 135.3)) I/flutter (19227): pointer move PointerMoveEvent#4726d(position: Offset(57.1, 137.8)) I/flutter (19227): pointer move PointerMoveEvent#82a6e(position: Offset(58.2, 156.4)) I/flutter (19227): pointer up PointerUpEvent#a3d76(position: Offset(58.2, 156.4)) ``` 可以看到flutter的事件模型和原生的事件模型是一样的。 #### 3.2 IgnorePointer、AbsorbPointer IgnorePointer和AbsorbPointer这两个组件用来阻止子Widget树接收指针事件,但是他们又有一些差异。 1. AbsorbPointer本身是可以接收指针事件的,子widget树不可以接受到指针事件。 2. IgnorePointer本身是不可以接收指针事件的,子widget树不可以接受到指针事件。 查看源码可知IgnorePointer和AbsorbPointer拦截子Widget的命中测试(Hit Test)。 IgnorePointer源码 ``` class IgnorePointer extends SingleChildRenderObjectWidget { ///是否忽略Pointer final bool ignoring; ...///省略代码 @override RenderIgnorePointer createRenderObject(BuildContext context) { return RenderIgnorePointer( ignoring: ignoring, ignoringSemantics: ignoringSemantics, ); } ...///省略代码 } class RenderIgnorePointer extends RenderProxyBox { bool get ignoring => _ignoring; bool _ignoring; ...///省略代码 @override bool hitTest(BoxHitTestResult result, { required Offset position }) { /// 如果忽略了,停止向下命中测试super.hitTest(result, position: position); return !ignoring && super.hitTest(result, position: position); } ...///省略代码 } ``` AbsorbPointer源码也是差不多同样的原理。 ### 4 手势识别 手势识别 (gesture) 是各种手势语义的抽象,封装了原始的指针事件,如点击 onTap、双击 onDoubleTap、长按 onLongPress、拖拽 onPanUpdate、缩放 onScaleUpdate 等。可以支持同时分发多个手势交互行为,同时监听多个事件。 #### 4.1 GestureDetector 关于组件层面flutter 提供了 GestureDetector,同时对一个Widget监听了多个手势事件,处理子 Widget 的手势识别和手势冲突(手势竞技场)。 ``` double _top = 0.0; //距顶部的偏移 double _left = 0.0;//距左边的偏移 String _operation = "A"; //保存事件名 double minRadius = 50; @override Widget build(BuildContext context) { return Stack( children: <Widget>[ Positioned( top: _top, left: _left, child: GestureDetector( child: CircleAvatar( minRadius: minRadius, child: Text(_operation)), onTap: () => updateText("Tap"), //点击 onDoubleTap: () => updateText("DoubleTap"), //双击 onLongPress: () => updateText("LongPress"), //长按 //手指按下时会触发此回调 onPanDown: (DragDownDetails e) { //打印手指按下的位置(相对于屏幕) print("用户手指按下:${e.globalPosition}"); }, //手指滑动时会触发此回调 onPanUpdate: (DragUpdateDetails e) { //用户手指滑动时,更新偏移,重新构建 print("用户手指滑动:${e.globalPosition}"); setState(() { _left += e.delta.dx; _top += e.delta.dy; }); }, onPanEnd: (DragEndDetails e){ print("用户手指滑动结束:$e"); //打印滑动结束时在x、y轴上的速度 print(e.velocity); }, onVerticalDragUpdate: (DragUpdateDetails details) { print("用户手指垂直更新:$e"); setState(() { _top += details.delta.dy; }); }, // 同时设置缩放会报错 Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. // onScaleUpdate: (ScaleUpdateDetails details) { // setState(() { // //缩放倍数在0.8到10倍之间 // minRadius = 200 * details.scale.clamp(.8, 10.0); // }); // }, ), ) ], ); } void updateText(String text) { //更新显示的事件名 setState(() { _operation = text; }); ``` <center>![](//img1.jcloudcs.com/developer.jdcloud.com/db403970-25ec-4b0f-951a-638982bb799e20220516152954.gif) 图1:拖拽效果图</center> 1.点击500毫秒内抬起触发点击监听,回调onTap,界面展示Tap。 2.点击不动超过500毫秒触发长按监听,回调onLongPress,界面展示LongPress。 3.双击触发双击监听,回调onDoubleTap,界面展示DoubleTap。 4.滑动首先判断滑动距离,dy>dx,垂直回调事件竞争胜出,回调onVerticalDragUpdate。 ``` I/flutter (20381): 用户手指垂直更新:DragUpdateDetails(Offset(0.0, -0.4)) I/flutter (20381): 用户手指垂直更新:DragUpdateDetails(Offset(0.0, -0.7)) I/flutter (20381): 用户手指垂直更新:DragUpdateDetails(Offset(0.0, -0.4)) ``` 5.由于没有设置onHorizontalDragUpdate,dy<dx,滑动回调事件竞争胜出,回调onPanUpdate。 ``` I/flutter (20381): 用户手指滑动:Offset(119.6, 420.4) I/flutter (20381): 用户手指滑动:Offset(119.6, 420.7) I/flutter (20381): 用户手指滑动结束:DragEndDetails(Velocity(0.0, 0.0)) I/flutter (20381): Velocity(0.0, 0.0) ``` 在上面的例子中,我们对一个 Widget 同时监听了多个手势事件,但最终只会有一个手势能够得到本次事件的处理权。对于多个手势的识别,flutter 引入了手势竞技场(Arena)的概念,用来识别究竟哪个手势可以响应用户事件。手势竞技场会考虑用户触摸屏幕的时长、位移以及拖动方向,来确定最终手势。 #### 4.2 手势竞技场 GestureDetector 内部对每一个手势都建立了一个工厂类(Gesture Factory)。而工厂类的内部会使用手势识别类(GestureRecognizer)来确定当前处理的手势,每一个手势识别器(GestureRecognizer)都是一个“竞争者”(GestureArenaMember),当发生指针事件时,他们都要在“竞技场”(Arena)去竞争本次事件的处理权,默认情况最终只有一个“竞争者”会胜出。 GestureDetector内部提供的手势竞争者有: - TapGestureRecognizer:点击事件识别器 - DoubleTapGestureRecognizer:双击事件识别器 - LongPressGestureRecognizer:长按事件识别器 - VerticalDragGestureRecognizer:垂直拖动识别器 - HorizontalDragGestureRecognizer:水平拖动识别器 - PanGestureRecognizer:拖动识别器 - ScaleGestureRecognizer:缩放识别器 - ForcePressGestureRecognizer:按压强度识别器 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/e96cb99c-6ebb-4c22-803e-4d4f404d4a3420220516153202.png) 图2:识别器类图</center> 如上类图可以看出所有的识别器都是竞争者(GestureArenaMember),可以理解竞技场内的竞争就是 GestureRecognizer 之间的竞争。 ### 5 自定义手势 使用RawGestureDetector,自定义GestureRecognizer实现自定义手势。熟悉图2中的系统提供的识别器的能力,扩展系统识别器满足不同场景自定义手势事件。 #### 5.1 点击事件 例如:子widget响应点击事件,父widget也响应点击事件。 ``` GestureDetector( onTap: () => print('Parent tapped'),//父视图的点击回调 child: Container( color: Colors.red, child: Center( child: GestureDetector( onTap: () => print('Child tapped'),//子视图的点击回调 child: Container( color: Colors.blue, width: 200.0, height: 200.0, ), ), ), ), ); ``` 当点击子视图打印如下: ``` I/flutter (5130): Child tapped ``` 手势竞技场里默认有一个胜出,并且事件会优先交给内层的控件响应,所以子控件打印了“Child tapped”,如果我们想父控件“Parent tapped”也能被打印,如何做呢? 1. 可以通过Listener + GestureDetector方式实现。 2. 可以通过自定义GestureRecognizer扩展系统识别器TapGestureRecognizer,识别器行为有:acceptGesture处理识别器竞技场竞争胜出调用,rejectGesture处理识别器竞技场竞争失败调用,我们可以在rejectGesture调用acceptGesture。 ``` ///自定义父Widget手势识别器, class ParentTapGestureRecognizer extends TapGestureRecognizer { @override void rejectGesture(int pointer) { ///竞争失败执行竞争成功 acceptGesture(pointer); } } ///自定义手势识别测试类 class TestGestureWidget extends StatelessWidget { @override Widget build(BuildContext context) { return RawGestureDetector( ///将自定义的手势识别类添加到RawGestureDetector中 gestures: { ParentTapGestureRecognizer:GestureRecognizerFactoryWithHandlers<ParentTapGestureRecognizer>( () => ParentTapGestureRecognizer(), (ParentTapGestureRecognizer instance){ instance.onTap = () => print("parent tapped");//点击回调 } ) }, child: Container( child: GestureDetector( onTap: (){ print('Child tapped'); }, child: Container( color: Colors.red, height: 100, width: 100, ), ), ), ); } } ``` 这样再点击子控件打印如下: ``` I/flutter (5130): Child tapped I/flutter (5130): parent tapped ``` #### 5.2 滑动事件 可以扩展VerticalDragGestureRecognizer、HorizontalDragGestureRecognizer等滑动识别器。 例如:垂直滑动事件。 1)继承系统识别器VerticalDragGestureRecognizer,自定义垂直滑动事件。 ``` ///1.监听child的位置更新 ///2.判断child在手松的那一刻是否是出于fling状态 class MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer { ///加速器 final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{} @override void handleEvent(PointerEvent event) { super.handleEvent(event); ///记录加速度 if (!event.synthesized && (event is PointerDownEvent || event is PointerMoveEvent)) { final VelocityTracker tracker = _velocityTrackers[event.pointer]; assert(tracker != null); tracker.addPosition(event.timeStamp, event.position); } ...///处理事件 } @override void addPointer(PointerEvent event) { super.addPointer(event); ///初始化加速器 _velocityTrackers[event.pointer] = VelocityTracker(); } ///滑动抬起回调 @override void didStopTrackingLastPointer(int pointer) { ...///处理滑动抬起处理 super.didStopTrackingLastPointer(pointer); } @override void dispose() { _velocityTrackers.clear(); super.dispose(); } } ``` 2)使用自定义识别器,并处理VerticalDragGestureRecognizer回调onStart、onUpdate、onEnd。 ``` GestureRecognizerFactoryWithHandlers<MyVerticalDragGestureRecognizer> getRecognizer() { return GestureRecognizerFactoryWithHandlers< MyVerticalDragGestureRecognizer>( () => MyVerticalDragGestureRecognizer(), //constructor (MyVerticalDragGestureRecognizer instance) { //initializer instance ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd; }, ); } void _handleDragEnd(DragEndDetails details) { ...///处理拖拽结束,处理fling } void _handleDragUpdate(DragUpdateDetails details) { ...///处理拖拽 } void _handleDragStart(DragStartDetails details) { ...///开始拖拽回调 } @override Widget build(BuildContext context) { return RawGestureDetector( gestures: {MyVerticalDragGestureRecognizer: getRecognizer()}, child: child, ); } ``` 通过VerticalDragGestureRecognizer回调函数onUpdate、onEnd,处理事件交互。 ### 6 总结 手势操作在 flutter 中分为两类:原始的指针事件(Pointer Event)、手势识别(Gesture Detector),原始的指针事件(Pointer Event)和移动端的事件模型基本一致的,手势识别(Gesture Detector)是在原始的指针事件(Pointer Event)的基础上进行的手势语义的封装。在开发中主要用的手势识别(Gesture Detector),手势识别内部有一个竞技场(Arena)的概念,手势识别器在竞技场内竞争胜利的会处理手势,默认一个成功其他的手势识别器都会被设置未失败,在默认手势识别器不能满足需求,我们可以自定义手势识别器在竞技场中获胜来满足我们的手势需求。 ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:霍明波
原创文章,需联系作者,授权转载
上一篇:分拣平台API安全治理实战
下一篇:CompletableFuture探索之旅
相关文章
前端十年回顾 | 漫画前端的前世今生
Taro小程序跨端开发入门实战
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号