Flutter 陈航 19-手势识别 PointerEvent GestureDetector GestureRecognizer
目录
19 | 用户交互事件该如何响应?
手势操作在 Flutter 中分为两类:
- 原始的指针事件(Pointer Event):即原生开发中常见的触摸事件,表示屏幕上触摸行为触发的位移行为
- 手势识别(Gesture Detector):表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装
Listener
指针事件表示用户交互的原始触摸数据,如手指接触屏幕 PointerDownEvent
、手指在屏幕上移动 PointerMoveEvent
、手指抬起 PointerUpEvent
、触摸取消 PointerCancelEvent
,这与原生系统的底层触摸事件抽象是一致的。
在手指接触屏幕,触摸事件发起时,Flutter 会确定手指与屏幕发生接触的位置上究竟有哪些组件,并将触摸事件交给最内层的组件去响应。与浏览器中的事件冒泡机制类似,事件会从这个最内层的组件开始,沿着组件树向根节点向上冒泡分发。
不过 Flutter 无法像浏览器冒泡那样取消或者停止事件进一步分发,只能通过 hitTestBehavior
去调整组件在命中测试期内应该如何表现,比如把触摸事件交给子组件,或者交给其视图层级之下的组件去响应。
Flutter 提供了 Listener Widget,可以监听其子 Widget 的原始指针事件。
Listener(
child: Container(color: Colors.blue, width: 200, height: 200),
onPointerDown: (event) => flog("按下 $event"),
onPointerMove: (event) => flog("移动 $event"),
onPointerUp: (event) => flog("抬起 $event"),
)
void flog(String text) {
log(text);
debugPrint(text);
}
GestureDetector
GestureDetector 是一个处理各种高级用户触摸行为的 Widget,与 Listener 一样,也是一个功能性组件。
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";
}
}
手势竞技场 Arena
尽管可以对一个 Widget 同时监听多个手势事件,但最终只会有一个手势能够得到本次事件的处理权。对于多个手势的识别,Flutter 引入了手势竞技场 Arena
,用来识别究竟哪个手势可以响应用户事件。
手势竞技场会考虑用户触摸屏幕的时长、位移、拖动方向,来确定最终手势。
实际上,GestureDetector 内部对每一个手势都建立了一个工厂类,而工厂类的内部会使用手势识别 GestureRecognizer 来确定当前处理的手势。
而所有手势的工厂类都会被交给 RawGestureDetector 类,以完成监测手势的大量工作:使用 Listener 监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器,然后这些手势会在竞技场决定最后由谁来响应用户事件。
改变竞技场行为
如果给多个存在父子关系的 Widget 注册同类型的手势监听器,手势竞技场通常最终会确认由 子 Widget 来响应事件。
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
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/17033128.html