真的是万万没想到,Flutter 中真的是“万物皆 Widget”,就连手势处理也是一个 Widget,也就是说,我们常用的单击、双击、长按甚至左滑右划也都是一个 Widget,是不是有些神奇?
先来看最基础的 —— GestureDetector。
在之前讲解 ListView 的时候,我们在 ListTile 中看到了关于点击的相关参数:onTap
、onLongPress
,但假如是我们自定义的 Item 布局呢? ListView 并没有提供相应的类似于 Android 那样的 setOnItemClickListener 这样的方法,我们在讲之前的基础控件的时候,也并没有发现和 Android setOnClickListener 那样的方法,那 Flutter 中是如何设置点击事件的呢?
事实上,在 Flutter 中,只要要将 Widget 作为 GestureDetector
的子控件,那这个 Widget 就可以相应我们的手势了,神器不神奇?
先看来一下 GestureDetector
的构造方法:
GestureDetector({
Key key,
this.child, //监听手势的控件
this.onTapDown, //手指开始触摸屏幕触发
this.onTapUp, //手指从屏幕抬起触发
this.onTap, //一次完成的单击事件结束后触发,onTapUp之后触发
this.onTapCancel, //触摸屏幕后,未完成触摸动作,即未抬起手指可触发,如长按方可触发改事件
this.onSecondaryTapDown, //下面三个有些疑惑,看了 API,查了一些资料,应该是和轨迹球、手写笔或者屏幕上的指针标识产生的 onTapDown、onTtapUp、TapCancel
this.onSecondaryTapUp, //同上
this.onSecondaryTapCancel, //同上
this.onDoubleTap, //短时间内触发屏幕两次触发
this.onLongPress, //长按触发
this.onLongPressStart, //长按开始时触发
this.onLongPressMoveUpdate, //长按时手指移动触发
this.onLongPressUp, //长按结束时触发
this.onLongPressEnd, //长按结束时触发,在onLongPressUp之前触发
this.onVerticalDragDown, //手指按下并在垂直方向上移动时触发
this.onVerticalDragStart, //当触摸点开始在垂直方向上移动时触发,在onVerticalDragDown之后触发
this.onVerticalDragUpdate, //触摸点在垂直方向上位置每发生改变就会触发
this.onVerticalDragEnd, //手指离开屏幕后方可触发
this.onVerticalDragCancel, //用户突然停止拖拽时触发
this.onHorizontalDragDown, //同onVerticalDragDown,水平方向
this.onHorizontalDragStart, //同onVerticalDragStart,水平方向
this.onHorizontalDragUpdate, //同onVerticalDragUpdate,水平方向
this.onHorizontalDragEnd, //同onVerticalDragEnd,水平方向
this.onHorizontalDragCancel, //同onVerticalDragCancel
this.onForcePressStart, //手指按压屏幕开始时触发(有压力检测的屏幕才能触发)
this.onForcePressPeak, //手指与屏幕接触并且压力达到最大时触发
this.onForcePressUpdate, //手指有足够的压力按压屏幕并在屏幕上移动时触发,
this.onForcePressEnd, //按压屏幕的手指松开时触发
this.onPanDown, //同onVerticalDragDown,任意方向
this.onPanStart, //同onVerticalDragStart,任意方向
this.onPanUpdate, //同onVerticalDragUpdate,任意方向
this.onPanEnd, //同onVerticalDragEnd,任意方向
this.onPanCancel, //同onVerticalDragCancel
this.onScaleStart, //双指开始捏合时触发
this.onScaleUpdate, //双指捏合缩放触发
this.onScaleEnd, // 双指结束结合后触发
this.behavior, //此手势检测器在命中测试期间应如何表现。如果child不为null,则默认为HitTestBehavior.deferToChild;如果child为null,则默认为HitTestBehavior.translucent。
this.excludeFromSemantics = false, //是否从语义树中排除这些手势。
this.dragStartBehavior = DragStartBehavior.start, //确定处理拖拽开始行为的方式
})
可以看到,除了 child,剩下的几乎都是跟手势相关的参数,我们需要做的,就是把我们需要监听手势的 Widget 作为 child 参数的值传入即可。
单击、双击和长按
简单的例子:
body: Stack(
children: <Widget>[
GestureDetector(
onTap: () {
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(SnackBar(
content: Text("点击了橘色区域"),
));
},
child: Container(
color: Colors.deepOrangeAccent,
width: 400,
height: 400,
),
),
GestureDetector(
onTap: () {
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(SnackBar(
content: Text("点击了淡蓝色区域"),
));
},
child: Container(
color: Colors.tealAccent,
width: 300,
height: 300,
),
),
Container(
color: Colors.deepPurple,
width: 100,
height: 100,
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(SnackBar(
content: Text("点击了文字"),
));
},
child: Text("Click me!"),
),
),
],
),
效果如下:
可以看到,点击、双击、长按不同的区域会触发相应的 tap,十分简单。
滑动拖动
除了单击双击,我们还经常会用到拖动和滑动的情况,这就需要获取手指在屏幕上的起始坐标以及移动坐标了。
拿 onPanDown
、onPanUpdate
和 onPanEnd
三个参数来说,他们分别是:GestureDragDownCallback
,GestureDragUpdateCallback
和 GestureDragEndCallback
类型的,我们详细看一下:
typedef GestureDragDownCallback = void Function(DragDownDetails details);
typedef GestureDragUpdateCallback = void Function(DragUpdateDetails details);
typedef GestureDragEndCallback = void Function(DragEndDetails details);
DragDownDetails({
this.globalPosition = Offset.zero, //手指在屏幕中按下的位置(原点为屏幕左上角)
Offset localPosition, //手指在控件中按下的位置(原点为控件左上角)
})
DragUpdateDetails({
this.sourceTimeStamp, //发动拖动事件的时间戳(但做测试的时候发现,好像并不是真正意义上的发生时间,有点儿像(可以计算)每个拖动事件的时间差的值)
this.delta = Offset.zero, //偏移量
this.primaryDelta, //主轴偏移量
@required this.globalPosition,//手指在屏幕中按下的位置(原点为屏幕左上角)
Offset localPosition,//手指在控件中按下的位置(原点为控件左上角)
})
DragEndDetails({
this.velocity = Velocity.zero, //代表用户抬起手指时的滑动速度(包含x、y两个轴的)
this.primaryVelocity, //用户抬起手指时沿主轴的移动速度
})
我们既然知道了手指在屏幕中的偏移量,那么自然也就可以动态的设置控件在屏幕中的位置了:
我们使用 Stack
作为父布局,这样就可以搭配 Positioned
的属性来设置子控件的位置了:
class _SwitchAndCheckBoxTestRouteState
extends State<SwitchAndCheckBoxTestRoute> {
double top = 100; //距顶部的偏移
double lfet = 100.0; //距左边的偏移
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("手势监听"),
),
body: Stack(
children: <Widget>[
Positioned(
top: top,
left: lfet,
child: GestureDetector(
child: Icon(Icons.ac_unit,size: 100,),
//手指按下时会触发此回调
onPanDown: (DragDownDetails e) {
//打印手指按下的位置(相对于屏幕)
print("用户手指按下:${e.globalPosition}");
print("用户手指按下:${e.localPosition}");
},
//手指滑动时会触发此回调
onPanUpdate: (DragUpdateDetails e) {
print("${e.delta.dx} * ${e.delta.dy}");
//用户手指滑动时,更新偏移,重新构建
setState(() {
lfet += e.delta.dx;
top += e.delta.dy;
});
},
onPanEnd: (DragEndDetails e) {
//打印滑动结束时在x、y轴上的速度
print(e.velocity.pixelsPerSecond.dx);
},
),
)
],
),
);
}
}
效果如下
在上面的例子中,我们可以横移纵移控件,在最一开始的参数中可以看到,我们也可以只设置某一方向的移动,或者是缩放,只需要在相应的回调中处理就可以了,这里不赘述。
事件拦截
先看一个示例:
body: Stack(
children: <Widget>[
GestureDetector(
onTap: () {
print("红色被点击");
},
child: Container(
width: 400,
height: 400,
color: Colors.deepOrange,
),
),
GestureDetector(
child: Container(
width: 300,
height: 300,
color: Colors.tealAccent,
),
),
],
),
效果是这样的:
点击红色区域,会有得到相应,但是点击蓝色区域,因为被蓝色区域覆盖掉了,所以并没有触发红色区域的 onTap,但是如果我们想要点击蓝色区域也会触发红色区域的 tap 方法呢(就是类似于 Android 中的不让蓝色区域拦截事件)?
Flutter 又提供了一种 Widget,你说说这个事儿闹的,又是一个 Widget,真的是万物皆是 Widget。
Flutter 提供了两个 Widget:
IgnorePointer
AbsorbPointer
二者都能组织其子控件接收事件,区别在于,AbsorbPointer
本身是可以接收指针事件的(但其子树不行),而IgnorePointer
本身就不可以接收指针事件,所以上面的例子这样修改一下就可以了:
body: Stack(
children: <Widget>[
GestureDetector(
onTap: () {
print("红色被点击 ${DateTime.now().millisecondsSinceEpoch}");
},
child: Container(
width: 400,
height: 400,
color: Colors.deepOrange,
)),
GestureDetector(
onTap: () {
print("蓝色被点击 ${DateTime.now().millisecondsSinceEpoch}");
},
child: IgnorePointer(
child: Container(
width: 300,
height: 300,
color: Colors.tealAccent,
),
),
),
],
),
这样点击蓝色区域,就不会触发蓝色区域的 onTap 事件了,而是触发红色区域的。
那么疑问来了,上面说了 AbsorbPointer
本身是可以接收事件的,但是你看它的 API,却并没有类似于 onTap
之类的,那是怎么回事儿呢?
事实上,Flutter 还有一个 Listener
,接下来看一下这个:
Listener
没错儿,它又是一个 Widget,哈哈哈。
我大概看了一下,这个玩意儿应该是一个简化版的 GestureDetector
,废话不多说,看源码:
const Listener({
Key key,
this.onPointerDown, //手指按下
this.onPointerMove, //手指一动
this.onPointerEnter, //进入Widget区域时触发
this.onPointerExit, //离开Widget区域时触发
this.onPointerHover, //
this.onPointerUp, //手指抬起
this.onPointerCancel, //取消回掉
this.onPointerSignal, //这个没明白是干啥的
this.behavior = HitTestBehavior.deferToChild, //事件传递
Widget child, //子控件
})
一个简单的例子:
body: Stack(
children: <Widget>[
Listener(
onPointerDown: (detail) {
print("手指按下");
},
onPointerMove: (detail) {
print("手指移动 ${detail.localPosition.dx}");
},
onPointerUp: (detail) {
print("手指抬起");
},
child: AbsorbPointer(
child: Listener(
onPointerDown: (detail) {
print("child手指按下");
},
onPointerMove: (detail) {
print("child手指移动 ${detail.localPosition.dx}");
},
onPointerUp: (detail) {
print("child手指抬起");
},
child: Container(
width: 300,
height: 300,
color: Colors.deepOrange,
),
),
)),
],
),
效果如下:
可以发现,child 中的 Listener
一系列事件并没有触发,而父 Listener 自身的一系列额事件却触发 了,但是如果将 AbsorbPointer
改为 IgnorePointer
,就都不会触发了,这里就不上效果图了。