一、基础事件监听(指针):Listener (功能性的widget)
指针事件表示用户交互的原始触摸数据,如手指接触屏幕 PointerDownEvent、手指在屏幕上移动 PointerMoveEvent、手指抬起 PointerUpEvent,以及触摸取消 PointerCancelEvent,这与原生系统的底层触摸事件抽象是一致的。
关于组件层面的原始指针事件的监听,Flutter
提供了 Listener Widget
,可以监听其子 Widget 的原始指针事件。
Listener(
child: Container(
color: Colors.red,//背景色红色
width: 300,
height: 300,
),
onPointerDown: (event) => print("down $event"),//手势按下回调
onPointerMove: (event) => print("move $event"),//手势移动回调
onPointerUp: (event) => print("up $event"),//手势抬起回调
);
二、手势:GestureDetector (功能性的widget)
通常情况下,响应用户交互行为的话,我们会使用封装了手势语义操作的 Gesture
,如点击 onTap
、双击 onDoubleTap
、长按 onLongPress
、拖拽onPanUpdate
、缩放 onScaleUpdate
等。另外,Gesture 可以支持同时分发多个手势交互行为,意味着我们可以通过 Gesture 同时监听多个事件。Gesture 是手势语义的抽象,而如果我们想从组件层监听手势,则需要使用 GestureDetector
。GestureDetector
是一个处理各种高级用户触摸行为的 Widget,与 Listener 一样,也是一个功能性组件。
定义一个场景:
我定义了一个 Stack
层叠布局,使用 Positioned
组件将 1 个红色的 Container
放置在左上角,并同时监听点击、双击、长按和拖拽事件。在拖拽事件的回调方法中,我们更新了 Container
的位置
//手势管理器
//红色container坐标
Stack(
//使用Stack组件去叠加视图,便于直接控制视图坐标
children: <Widget>[
Positioned(
top: _top,
left: _left,
child: GestureDetector(
//手势识别
child:
Container(color: Colors.red, width: 50, height: 50), //红色子视图
onTap: () => print("Tap"), //点击回调
onDoubleTap: () => print("Double Tap"), //双击回调
onLongPress: () => print("Long Press"), //长按回调
onPanUpdate: (e) {
//拖动回调
setState(() {
//更新位置
_left += e.delta.dx;
_top += e.delta.dy;
});
},
),
)
],
),
可以测试点击、双击、长按以及拖动事件
三、多手势识别:手势竞技场(Arena)
尽管在上面的例子中,我们对一个 Widget 同时监听了多个手势事件,但最终只会有一个手势能够得到本次事件的处理权。对于多个手势的识别,Flutter 引入了手势竞技场(Arena)的概念,用来识别究竟哪个手势可以响应用户事件。手势竞技场会考虑用户触摸屏幕的时长、位移以及拖动方向,来确定最终手势。
那手势竞技场具体是怎么实现的呢?
实际上,GestureDetector 内部对每一个手势都建立了一个工厂类(Gesture Factory)。而工厂类的内部会使用手势识别类(GestureRecognizer),来确定当前处理的手势。而所有手势的工厂类都会被交给 RawGestureDetector 类,以完成监测手势的大量工作:使用 Listener 监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器,然后这些手势会在竞技场决定最后由谁来响应用户事件。
有些时候我们可能会在应用中给多个视图注册同类型的手势监听器,比如点击不同区域会有不同的响应:
像这样的手势识别发生在多个存在父子关系的视图时,手势竞技场会一并检查父视图和子视图的手势,并且通常最终会确认由子视图来响应事件。而这也是合乎常理的:从视觉效果上看,子视图的视图层级位于父视图之上,相当于对其进行了遮挡,因此从事件处理上看,子视图自然是事件响应的第一责任人。在下面的示例中,我定义了两个嵌套的 Container 容器,分别加入了点击识别事件:
GestureDetector(
onTap: () => print('Parent tapped'),//父视图的点击回调
child: Container(
color: Colors.pinkAccent,
child: Center(
child: GestureDetector(
onTap: () => print('Child tapped'),//子视图的点击回调
child: Container(
color: Colors.blueAccent,
width: 200.0,
height: 200.0,
),
),
),
),
),
效果:父子容器嵌套,然后在蓝色区域进行点击,可以发现:尽管父容器也监听了点击事件,但 Flutter 只响应了子容器的点击事件。
如果我们也想父容器也响应时间呢?
为了让父容器也能接收到手势,我们需要同时使用 RawGestureDetector 和 GestureFactory,来改变竞技场决定由谁来响应用户事件的结果。
在此之前,我们还需要自定义一个手势识别器,让这个识别器在竞技场被 PK 失败时,能够再把自己重新添加回来,以便接下来还能继续去响应用户事件。
在下面的代码中,我定义了一个继承自点击手势识别器 TapGestureRecognizer 的类,并重写了其 rejectGesture 方法,手动地把自己又复活了:
class MultipleTapGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
接下来,我们需要将手势识别器和其工厂类传递给 RawGestureDetector,以便用户产生手势交互事件时能够立刻找到对应的识别方法。事实上,RawGestureDetector 的初始化函数所做的配置工作,就是定义不同手势识别器和其工厂类的映射关系。
这里,由于我们只需要处理点击事件,所以只配置一个识别器即可。工厂类的初始化采用 GestureRecognizerFactoryWithHandlers 函数完成,这个函数提供了手势识别对象创建,以及对应的初始化入口。
在下面的代码中,我们完成了自定义手势识别器的创建,并设置了点击事件回调方法。
需要注意的是,由于我们只需要在父容器监听子容器的点击事件,所以只需要将父容器用 RawGestureDetector 包装起来就可以了,而子容器保持不变:
RawGestureDetector(//自己构造父Widget的手势识别映射关系
gestures: {
//建立多手势识别器与手势识别工厂类的映射关系,从而返回可以响应该手势的recognizer
MultipleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
MultipleTapGestureRecognizer>(
() => MultipleTapGestureRecognizer(),
(MultipleTapGestureRecognizer instance) {
instance.onTap = () => print('parent tapped ');//点击回调
},
)
},
child: Container(
color: Colors.pinkAccent,
child: Center(
child: GestureDetector(//子视图可以继续使用GestureDetector
onTap: () => print('Child tapped'),
child: Container(
color: Colors.blueAccent,
width: 200.0,
height: 200.0,
),
),
),
),
),
可以看到:点击子容器的时候父容器随后收到了事件响应