目录
目录19 | 用户交互事件该如何响应?
手势操作在 Flutter 中分为两类:
- 第一类是原始的指针事件(Pointer Event),即原生开发中常见的触摸事件,表示屏幕上触摸行为触发的位移行为
- 第二类则是手势识别(Gesture Detector),表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装
指针事件 Listener
指针事件表示用户交互的原始触摸数据,如手指接触屏幕 PointerDownEvent
、手指在屏幕上移动 PointerMoveEvent
、手指抬起 PointerUpEvent
,以及触摸取消 PointerCancelEvent
,这与原生系统的底层触摸事件抽象是一致的。
在手指接触屏幕,触摸事件发起时,Flutter 会确定手指与屏幕发生接触的位置上究竟有哪些组件,并将触摸事件交给最内层的组件去响应。与浏览器中的事件冒泡机制类似,事件会从这个最内层的组件开始,沿着组件树向根节点向上冒泡分发。
不过 Flutter 无法像浏览器冒泡那样取消或者停止事件进一步分发,我们只能通过 hitTestBehavior
去调整组件在命中测试期内应该如何表现,比如把触摸事件交给子组件,或者交给其视图层级之下的组件去响应。
Flutter 提供了 Listener Widget,可以监听其子 Widget 的原始指针事件。
Listener
Listener(
child: Container(color: Colors.blue, width: 200, height: 200),
onPointerDown: (event) => flog("按下 $event"),
onPointerMove: (event) => flog("移动 $event"),
onPointerUp: (event) => flog("抬起 $event"),
)
完整代码
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'flog.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(home: HomePage(title: '白乾涛'));
}
class HomePage extends StatefulWidget {
final String title;
const HomePage({super.key, required this.title});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Listener(...),
floatingActionButton: FloatingActionButton(
onPressed: () => flog("onPressed"),
child: const Text("按钮"),
));
}
}
import 'dart:developer';
import 'package:flutter/widgets.dart';
void flog(String text) {
log(text);
debugPrint(text);
}
手势识别
如果我们想从组件层监听手势,则需要使用 GestureDetector。GestureDetector 是一个处理各种高级用户触摸行为的 Widget,与 Listener 一样,也是一个功能性组件。
GestureDetector
Stack(
children: <Widget>[
Positioned(
top: _top,
left: _left,
child: GestureDetector(
child: Container(color: Colors.red, width: 200, height: 200),
onTap: () => flog("点击"),
onDoubleTap: () => flog("双击"),
onLongPress: () => flog("长按"),
onPanUpdate: (e) => flog("拖拽 ${e.delta.toString()}"),
),
)
],
),
拖拽和缩放
拖拽 onPanUpdate
和 缩放 onScaleUpdate
不可以同时使用
onPanUpdate: (e) => setState(() {
flog("拖拽 ${e.delta.info(1)}");
_left += e.delta.dx;
_top += e.delta.dy;
}),
onScaleUpdate: (details) => setState(() {
flog("缩放 scale= ${details.scale} ${details.horizontalScale} ${details.verticalScale}");
_width = 200 * details.horizontalScale;
_height = 200 * details.verticalScale;
})
extension on Offset {
String info([int? fractionDigits]) {
var dxValue = dx.toStringAsExponential(fractionDigits);
var dyValue = dy.toStringAsExponential(fractionDigits);
return "dx=$dxValue dy=$dyValue";
}
}
手势竞技场
尽管我们可以对一个 Widget 同时监听多个手势事件,但最终只会有一个手势能够得到本次事件的处理权。对于多个手势的识别,Flutter 引入了手势竞技场(Arena),用来识别究竟哪个手势可以响应用户事件。
手势竞技场会考虑用户触摸屏幕的时长、位移以及拖动方向,来确定最终手势。
实际上,GestureDetector 内部对每一个手势都建立了一个工厂类。而工厂类的内部会使用手势识别 GestureRecognizer 来确定当前处理的手势。
而所有手势的工厂类都会被交给 RawGestureDetector 类,以完成监测手势的大量工作:使用 Listener 监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器,然后这些手势会在竞技场决定最后由谁来响应用户事件。
竞技场默认行为
有些时候我们可能会在应用中给多个视图注册同类型的手势监听器,像这样的手势识别发生在多个存在父子关系的视图时,手势竞技场会一并检查父视图和子视图的手势,并且通常最终会确认由子视图来响应事件。
GestureDetector(
onTap: () => flog('父视图的点击回调'),
child: Container(
color: Colors.green,
child: Center(
child: GestureDetector(
onTap: () => flog('子视图的点击回调'),
child: Container(color: Colors.blue, width: 200, height: 200),
),
),
),
)
改变竞技场行为
为了让父容器也能接收到手势,我们需要同时使用 RawGestureDetector 和 GestureFactory,来改变竞技场决定由谁来响应用户事件的结果。
GestureRecognizer
GestureDetector 内部对每一个手势都建立了一个工厂类,工厂类的内部会使用手势识别类 GestureRecognizer 来确定当前要处理的手势。
TapGestureRecognizer 继承自 GestureRecognizer
/// 自定义点击手势识别器,让其在竞技场被 PK 失败时,把自己重新添加回来,以便接下来能继续响应手势事件
class MyTapGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
flog("rejectGesture(拒绝响应手势) pointer=$pointer");
acceptGesture(pointer);
}
}
GestureRecognizerFactory
GestureRecognizerFactory 用于工厂类的初始化,其提供了手势识别对象创建(_constructor
),以及对应的初始化入口(_initializer
)。
Factory for creating
gesture recognizers
that delegates to callbacks.
GestureRecognizerFactoryWithHandlers 继承自 GestureRecognizerFactory
/// 定义手势识别工厂的初始化逻辑
var _gestureRecognizerFactory = GestureRecognizerFactoryWithHandlers<MyTapGestureRecognizer>(
_constructor,
_initializer,
);
/// 提供手势识别对象创建的方法
MyTapGestureRecognizer Function() _constructor = () {
flog("_constructor");
return MyTapGestureRecognizer();
};
/// 提供初始化方法
_initializer(MyTapGestureRecognizer instance) {
flog('_initializer');
instance.onTap = () => flog('instance.onTap');
}
建立映射关系
建立多手势识别器 GestureRecognizer 与手势识别工厂 GestureRecognizerFactory 的映射关系
/// 用于建立多 手势识别器 GestureRecognizer 与手势识别工厂 GestureRecognizerFactory 的映射关系
Map<Type, GestureRecognizerFactory<MyTapGestureRecognizer>> _gestures = {
MyTapGestureRecognizer: _gestureRecognizerFactory,
};
RawGestureDetector
将手势识别器和其工厂类 通过 属性 gestures 传递给 RawGestureDetector,以便用户产生手势交互事件时能够立刻找到对应的识别方法。事实上,RawGestureDetector 的初始化函数所做的配置工作,就是定义不同手势识别器和其工厂类的映射关系。
由于我们只需要在父容器监听子容器的点击事件,所以只需要将父容器用 RawGestureDetector 包装起来就可以了,而子容器保持不变。
RawGestureDetector(
gestures: _gestures, // 构造父 Widget 的手势识别映射关系
child: Container(
color: Colors.green,
child: Center(
child: GestureDetector(
onTap: () => flog('子视图的点击回调'), // 子视图可以继续使用 GestureDetector
child: Container(color: Colors.blue, width: 200, height: 200),
),
),
),
),
效果
// 初始化
[log] _constructor
[log] _initializer
[log] _initializer
// 点击子视图区域
[log] 子视图的点击回调
[log] rejectGesture(拒绝响应手势) pointer=7
[log] instance.onTap
// 点击父视图区域
[log] instance.onTap
总结
在 Flutter 中,尽管我们可以对一个 Widget 监听多个手势,或是对多个 Widget 监听同一个手势,但 Flutter 会使用手势竞技场来进行各个手势的 PK,以保证最终只会有一个手势能够响应用户行为。如果我们希望同时能有多个手势去响应用户行为,需要去自定义手势,利用 RawGestureDetector 和手势工厂类,在竞技场 PK 失败时,手动把它复活。
在处理多个手势识别场景,很容易出现手势冲突的问题。比如,当需要对图片进行点击、长按、旋转、缩放、拖动等操作的时候,如何识别用户当前是点击还是长按,是旋转还是缩放。如果想要精确地处理复杂交互手势,我们势必需要介入手势识别过程解决异常。
不过需要注意的是,冲突的只是手势的语义化识别过程,原始指针事件是不会冲突的。所以,在遇到复杂的冲突场景通过手势很难搞定时,我们也可以通过 Listener 直接识别原始指针事件,从而解决手势识别的冲突。
2023-1-7
标签:GestureDetector,PointerEvent,GestureRecognizer,flog,视图,事件,识别,手势 From: https://www.cnblogs.com/baiqiantao/p/17033128.html