本篇主要讲解Flutter的事件处理相关的内容:Pointer事件处理、手势识别GestureDetector。
Flutter中的手势系统有两个独立的层。第一层有原始指针(pointer)事件,它描述了屏幕上指针(例如,触摸,鼠标和触控笔)的位置和移动。 第二层有手势,描述由一个或多个指针移动组成的语义动作。
1. Pointer
指针(Pointer)代表用户与设备屏幕交互的原始数据。有四种类型的指针
-
PointerDownEvent
指针接触到屏幕的特定位置 -
PointerMoveEvent
指针从屏幕上的一个位置移动到另一个位置 -
PointerUpEvent
指针停止接触屏幕 -
PointerCancelEvent
指针的输入事件不再针对此应用(事件取消)
它们均为 PointerEvent 的子类,包含如下一些信息
-
position
:它是鼠标相对于当对于全局坐标的偏移。 -
delta
:两次指针移动事件(PointerMoveEvent
)的距离。 -
pressure
:按压力度,如果手机屏幕支持压力传感器(如iPhone的3D Touch),此属性会更有意义,如果手机不支持,则始终为1。 -
orientation
:指针移动方向,是一个角度值。
除了这些它还有很多属性。此处不再赘述可自行查看API文档。
当屏幕上指针按下时,框架会对应用程序执行命中测试,以确定指针与屏幕相接的位置存在哪些widget。 指针按下事件(以及该指针的后续事件)然后被分发到由命中测试发现的最内部的widget。 从那里开始,这些事件会冒泡在widget树中向上冒泡,这些事件会从最内部的widget被分发到到widget根的路径上的所有小部件(译者语:这和web中事件冒泡机制相似), 没有机制取消或停止冒泡过程(译者语:这和web不同,web中的时间冒泡是可以停止的)。当然这也和 iOS中的响应者链的概念有一丢丢类似。
Flutter中可以使用Listener
来监听原始触摸事件,Listener
也是一个功能性组件。下面是Listener
的构造函数定义:
Listener({
Key key,
this.onPointerDown, //手指按下回调
this.onPointerMove, //手指移动回调
this.onPointerUp,//手指抬起回调
this.onPointerCancel,//触摸事件取消回调
this.behavior = HitTestBehavior.deferToChild, //在命中测试期间如何表现
Widget child
})
可以来看个小例子:
//定义一个状态,保存当前指针位置
PointerEvent _event;
Listener(
// 定义触摸区域
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 300.0,
height: 150.0,
// 添加文本控件显示移动距离
child: Text(_event?.toString()??"",style: TextStyle(color: Colors.white)),
),
// 不同的时间响应处理
onPointerDown: (PointerDownEvent event) => setState(()=>_event=event),
onPointerMove: (PointerMoveEvent event) => setState(()=>_event=event),
onPointerUp: (PointerUpEvent event) => setState(()=>_event=event),
),
当然要直接从widget层监听指针事件,可以使用Listener
widget。 但是,更常见的考虑使用第2章节所讲述的手势
1.1 忽略PointerEvent
假如我们不想让某个子树响应PointerEvent
的话,我们可以使用IgnorePointer
和AbsorbPointer
,这两个组件都能阻止子树接收指针事件,不同之处在于AbsorbPointer
本身会参与命中测试,而IgnorePointer
本身不会参与,这就意味着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. 手势
手势表示可以从多个单独的指针事件(甚至可能是多个单独的指针)识别的语义动作(例如,轻敲,拖动和缩放)。 完整的一个手势可以分派多个事件,对应于手势的生命周期(例如,拖动开始,拖动更新和拖动结束):
- Tap
-
onTapDown
指针已经在特定位置与屏幕接触 -
onTapUp
指针停止在特定位置与屏幕接触 -
onTap
tap事件触发 -
onTapCancel
先前指针触发的onTapDown
不会在触发tap事件
-
- 双击
-
onDoubleTap
用户快速连续两次在同一位置轻敲屏幕.
-
- 长按
-
onLongPress
指针在相同位置长时间保持与屏幕接触
-
- 垂直拖动
-
onVerticalDragStart
指针已经与屏幕接触并可能开始垂直移动 -
onVerticalDragUpdate
指针与屏幕接触并已沿垂直方向移动. -
onVerticalDragEnd
先前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动
-
- 水平拖动
-
onHorizontalDragStart
指针已经接触到屏幕并可能开始水平移动 -
onHorizontalDragUpdate
指针与屏幕接触并已沿水平方向移动 -
onHorizontalDragEnd
先前与屏幕接触并水平移动的指针不再与屏幕接触,并在停止接触屏幕时以特定速度移动
-
要从widget层监听手势,请使用 GestureDetector
.GestureDetector
是一个用于手势识别的功能性组件,我们通过它可以来识别各种手势。下面我们来看三个简单的使用例子:
class GestureDetectorTestRoute extends StatefulWidget {
@override
_GestureDetectorTestRouteState createState() =>
new _GestureDetectorTestRouteState();
}
class _GestureDetectorTestRouteState extends State {
String _operation = "No Gesture detected!"; //保存事件名
@override
Widget 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;
});
}
}
class ScaleTestRouteState extends State {
double _width = 200.0; //通过修改图片宽度来达到缩放效果
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
//指定宽度,高度自适应
child: Image.asset("lib/assets/images/test.png", width: _width),
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
//缩放倍数在0.8到10倍之间
_width=200*details.scale.clamp(.5, 10.0);
});
},
),
);
}
}
class Drag extends StatefulWidget {
@override
DragState createState() => new DragState();
}
class DragState extends State with SingleTickerProviderStateMixin {
double _top = 0.0; //距顶部的偏移
double _left = 0.0;//距左边的偏移
@override
Widget build(BuildContext context) {
return Stack(
children: [
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);
},
),
)
],
);
}
}
2.1 手势消歧
在屏幕上的指定位置,可能会有多个手势检测器。所有这些手势检测器在指针事件流经过并尝试识别特定手势时监听指针事件流。 GestureDetector
widget决定是哪种手势。
当屏幕上给定指针有多个手势识别器时,框架通过让每个识别器加入一个“手势竞争场”来确定用户想要的手势。“手势竞争场”使用以下规则确定哪个手势胜出
- 在任何时候,识别者都可以宣布失败并离开“手势竞争场”。如果在“竞争场”中只剩下一个识别器,那么该识别器就是赢家
- 在任何时候,识别者都可以宣布胜利,这会导致胜利,并且所有剩下的识别器都会失败
例如,在消除水平和垂直拖动的歧义时,两个识别器在接收到指针向下事件时进入“手势竞争场”。识别器观察指针移动事件。 如果用户将指针水平移动超过一定数量的逻辑像素,则水平识别器将声明胜利,并且手势将被解释为水平拖拽。 类似地,如果用户垂直移动超过一定数量的逻辑像素,垂直识别器将宣布胜利。
当只有水平(或垂直)拖动识别器时,“手势竞争场”是有益的。在这种情况下,“手势竞争场”将只有一个识别器,并且水平拖动将被立即识别,这意味着水平移动的第一个像素可以被视为拖动,用户不需要等待进一步的手势消歧。