Flutter 学习之旅07 事件处理


1 原始指针事件

1.1 基本概念

一个完整的原始指针事件主要由手指按下、手指移动、手指抬起以及触摸取消构成,更高基本的手势都基于这些原始事件。

在Flutter的原始指针事件模型中,在手指接触屏幕发起触摸事件时,Flutter会首先确定手指与屏幕发生接触的位置上究竟有哪些组件,然后通过命中测试(Hit Test)交给最内层的组件去响应。

Flutter无法像浏览器冒泡那样取消或者停止事件的进一步分发,只能通过执行命中测试去调整组件的事件触发时机。

PointerDownEvent、PointerMoveEvent和PointerUpEvent是Flutter的原始指针事件的基本组成部分,分别对应手指按下、移动和抬起事件,它们都是PointerEvent的子类。

在Flutter的事件模型中PointerEvent是Flutter原始指针事件的基础类,可以用它获取当前指针的一些信息:

1)position:全局坐标的偏移量;

2)delta:两次指针移动事件的距离;

3)pressure:按压力度,如果手机屏幕支持压力传感器,此属性会返回压力值,如果手机不支持则始终返回1;

4)orientation:指针移动方向,是一个角度值。

对于组件层面的原始指针事件的监听,Flutter提供了一个Listener,可以用它监听包裹的子组件的原始指针事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Listener(
onPointerDown: (downPointEvent) {
//按下回调
...
},
onPointerMove: (movePointEvent) {
//移动回调
...
},
onPointerUp: (upPointEvent) {
//抬起回调
...
},
child: Container(
child: Text(‘Listener事件监听’);
)
)

原始指针事件还提供了behavior属性,它决定子组件如何响应命中测试,它的值类型为HitTestBehavior,是一个枚举类型,有3个枚举值:

1)deferToChild:子组件一个接一个地进行命中测试,如果子组件中有通过命中测试的,则当前组件会收到指针事件,并且其父组件会收到指针事件;

2)opaque:在进行命中测试时,当前组件会被当成不透明进行处理,单击的响应区域即为单击区域;

3)translucent:设置此属性后,组件自身和底部可视区域都能够响应命中测试,即点击顶部组件时,顶部组件和底部组件都可以接收到指针事件。

1.2 忽略事件

如果不想让某个子组件响应原始指针事件,可以使用AbsorbPointer或IgnorePointer组件包裹子组件来阻止子组件接收指针事件。

AbsorbPointer组件会参与命中测试,它本身可以接收指针事件,其包裹的子组件不能;而IgnorePointer组件不会参与命中测试,它完全不能接收指针事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'package:flutter/material.dart';

void main() => runApp(PointerWidget());

class PointerWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理',
home: Scaffold(
appBar: AppBar(title: Text('事件处理')),
body: Listener(
child: AbsorbPointer(
child: Listener(
child: Container(
color: Colors.red,
width: 200,
height: 200,
),
onPointerDown: (e) => print('point in'),
)
),
onPointerDown: (e) => print('point up'),
)
)
);
}
}

2 手势识别组件

2.1 基本用法

在Flutter开发中,Gesture API代表手势语义的抽象,从组件层面监听手势可以使用GestureDetector等手势响应组件。

GestureDetector组件是一个处理各种高级用户触摸行为的组件,使用时只需要将它作为父组件包裹在其他子组件外面即可。

2.2 常用事件

GestureDetector常用事件:

img

如果同时监听onTap和onDoubleTap事件时,onTap事件会有200ms左右的延迟。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import 'package:flutter/material.dart';

void main() => runApp(GestureDetectorPage());

class GestureDetectorPage extends StatefulWidget {
State<StatefulWidget> createState() {
return GestureDetectorPageState();
}
}

class GestureDetectorPageState extends State<GestureDetectorPage> {
String operation = 'No Gesture';

void updateGesture(String text) {
setState(() => operation = text);
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理',
home: Scaffold(
appBar: AppBar(title: Text('事件处理 -- $operation')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
child: Container(
margin: EdgeInsets.only(top: 20),
alignment: Alignment.center,
color: Colors.blue,
width: 150,
height: 80,
child: Text(
operation,
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
onTap: () => updateGesture('Tap'),
onDoubleTap: () => updateGesture('DoubleTap'),
onLongPress: () => updateGesture('LongPress'),
)
],
)
)
);
}
}

示例效果:

img

2.3 拖拽与缩放

在处理拖拽事件时,GestureDetector会将需要监听组件的原点作为本次手势的起点,当用户在监听组件上按下手指时手势识别就开始运行。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import 'package:flutter/material.dart';

void main() => runApp(DragPage());

class DragPage extends StatefulWidget {
State<StatefulWidget> createState() {
return DragState();
}
}

class DragState extends State<DragPage> {
String operation = 'No Gesture';

void updateGesture(String text) {
setState(() => operation = text);
}
double _top = 0.0;
double _left = 0.0;

@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理--拖拽',
home: Scaffold(
appBar: AppBar(title: Text('事件处理--拖拽')),
body: Stack(
children: <Widget>[
Positioned(
top: _top,
left: _left,
child: GestureDetector(
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.blue,
child: Text(
'Drag',
style: TextStyle(color: Colors.white),
),
),
onPanDown: (DragDownDetails e) => print('onPanDown: ${e.globalPosition}'),
onPanUpdate: (DragUpdateDetails e) {
setState(() {
_left += e.delta.dx;
_top += e.delta.dy;
});
},
onPanEnd: (DragEndDetails e) => print('onPanEnd: ${e.velocity.toString()}'),
),
)
],
)
)
);
}
}

示例效果:

img

如果只需要沿一个方向拖动,可以将onPanUpdate改为onVerticalDragUpdate或者onHorizontalDragUpdate。

可以使用GestureDetector组件的onScaleUpdate实现缩放效果。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import 'package:flutter/material.dart';

void main() => runApp(ScalePage());

class ScalePage extends StatefulWidget {
State<StatefulWidget> createState() {
return ScaleState();
}
}

class ScaleState extends State<ScalePage> {
double width = 100.0;

@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理--缩放',
home: Scaffold(
appBar: AppBar(title: Text('事件处理--缩放')),
body: Center(
child: GestureDetector(
child: Image.asset('images/qq.png', width: width),
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
width = 200 * details.scale.clamp(0.8, 10.0);
});
},
),
)
)
);
}
}
2.4 手势识别器

GestureDetector封装了Listener的原始指针事件,可以很容易地对各种手势进行识别。GestureDetector是一个抽象类,有多个实现子类,通常一种手势识别器即对应一个GestureDetector的实现类。

示例代码:动态改变富文本文字大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

void main() => runApp(GestureRecognizerPage());

class GestureRecognizerPage extends StatefulWidget {
State<StatefulWidget> createState() {
return GestureRecognizerState();
}
}

class GestureRecognizerState extends State<GestureRecognizerPage> {
TapGestureRecognizer recognizer = TapGestureRecognizer();
bool toggle = false;

@override
void dispose() {
recognizer.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理--手势识别器',
home: Scaffold(
appBar: AppBar(title: Text('事件处理--手势识别器')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('点击文字放大'),
Text.rich(
TextSpan(
text: '你好,Flutter',
style: TextStyle(fontSize: toggle ? 30 : 60),
recognizer: recognizer..onTap = () {
setState(() {
toggle = !toggle;
});
}
),
)
],
)
)
)
);
}
}

示例效果:

img

使用手势识别器后一定要调用dispose()来释放资源,因为手势识别器内部使用了计时器,不释放的话会造成大量的资源消耗。

2.5 手势竞争

对于需要处理多个手势识别的场景,Flutter引入了手势竞技场的概念,用来识别究竟哪个手势最终响应用户事件。手势竞技场通过综合对比用户触摸屏幕的时长、位移以及拖拽方向来确定最终手势。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

void main() => runApp(GestureCompetePage());

class GestureCompetePage extends StatefulWidget {
State<StatefulWidget> createState() {
return GestureCompeteState();
}
}

class GestureCompeteState extends State<GestureCompetePage> {
double _top = 0.0;
double _left = 0.0;

@override
Widget build(BuildContext context) {
return MaterialApp(
title: '事件处理--手势竞争',
home: Scaffold(
appBar: AppBar(title: Text('事件处理--手势竞争')),
body: Stack(
children: <Widget>[
Positioned(
top: _top,
left: _left,
child: GestureDetector(
child: CircleAvatar(
minRadius: 40,
child: Text(
'手势竞争',
style: TextStyle(fontSize: 16)
),
),
onVerticalDragUpdate: (DragUpdateDetails details) {
setState(() {
_top += details.delta.dy;
});
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_left += details.delta.dx;
});
},
)
)
],
)
)
);
}
}

示例效果:

img

每次拖动小球时,小球只会沿着一个方向移动。