Flutter学习(二)——触摸事件和手势

各种 Widget 进行组合之后,可以得到我们需要的画面。这肯定是不够的,我们需要与页面元素进行交互。这也就是本文所要介绍的 Pointer Event(指针事件) 和 Gesture(手势)。

这篇文章简单介绍一下指针事件和手势的使用方式。

Pointer Event

在移动设备上,Point Event 也就是我们熟悉的触摸事件。当然,Flutter 可支持的平台众多,这个 Pointer Event 的触发者可能是由手指、鼠标、触摸板等。但是,无论触发者是谁,这个指针事件的处理阶段还是被分为了四个阶段:手指按下、手指移动、手指抬起或事件取消。

  • PointerDownEvent

手指按下时,Flutter 会开始执行 Hit Test 来确定当前触摸事件点击位置存在哪些组件,会按照深度优先遍历当前渲染树,对每一个渲染对象进行 Hit Test,如果命中测试通过,则该渲染对象会被添加到一个 HitTestResult 列表当中。

  • PointerMoveEvent

命中测试完毕后,会遍历 HitTestResult 列表,调用每一个渲染对象的事件处理方法(handleEvent)来处理 PointerDownEvent 事件,该过程称为“事件分发”(event dispatch)。随后当手指移动时,便会分发 PointerMoveEvent 事件。

  • PointerUpEvent、PointerCancelEvent

对相应的事件进行分发,分发完毕后会清空 HitTestResult 列表。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
PointerEvent? _event;

@override
Widget build(BuildContext context) {
return Listener(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
child: Text(
'是兄弟就来摸我!${_event?.localPosition ?? ''}',
style: TextStyle(color: Colors.white),
),
),
onPointerDown: (PointerDownEvent event) => setState(() => _event = event),
onPointerMove: (PointerMoveEvent event) => setState(() => _event = event),
onPointerUp: (PointerUpEvent event) => setState(() => _event = event),
);
}
}

PointerDownEvent、 PointerMoveEvent、 PointerUpEvent 都是 PointerEvent 的子类,PointerEvent 类中包括当前指针的一些信息。这里罗列一些常用的:

  • position:它是指针相对于当对于全局坐标的偏移。
  • localPosition: 它是指针相对于当对于本身布局坐标的偏移。
  • delta:两次指针移动事件(PointerMoveEvent)的距离。
  • pressure:按压力度,如果手机屏幕支持压力传感器(如 iPhone 的 3D Touch),此属性会更有意义,如果手机不支持,则始终为 1。
  • orientation:指针移动方向,是一个角度值。

忽略指针事件

使用 IgnorePointer 和 AbsorbPointer,这两个组件都能阻止子树接收指针事件。

  • AbsorbPointer,会参与命中测试。AbsorbPointer 本身是可以接收指针事件的(但其子树不行)。
  • IgnorePointer,本身不会参与命中测试。

Gesture

GestureDetector

GestureDetector 是一个用于手势识别的功能性组件,我们通过它可以来识别各种手势。GestureDetector 内部封装了 Listener,用以识别语义化的手势。GestureDetector 直接可以接收一个子 widget。

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
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
String _operation = "No Gesture detected!"; //保存事件名

void updateText(String text) {
//更新显示的事件名
setState(() {
_operation = text;
});
}

@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 350.0,
height: 200.0,
child: Text(
_operation,
style: TextStyle(color: Colors.white),
),
),
onTap: () => updateText("Tap"), //点击
onDoubleTap: () => updateText("DoubleTap"), //双击
onLongPress: () => updateText("LongPress"), //长按

//手指按下时会触发此回调
// onPanDown: (DragDownDetails e) {
// //打印手指按下的位置(相对于屏幕)
// updateText("PanDown");
// },
// //手指滑动时会触发此回调
// onPanUpdate: (DragUpdateDetails e) {
// //用户手指滑动时,更新偏移,重新构建
// updateText("PanUpdate");
// },
// onPanEnd: (DragEndDetails e){
// //打印滑动结束时在x、y轴上的速度
// updateText("PanEnd");
// },
// onVerticalDragUpdate: (DragUpdateDetails details) {
// updateText("VerticalDragUpdate");
// },
// onHorizontalDragUpdate: (DragUpdateDetails details) {
// updateText("HorizontalDragUpdate");
// },
// onScaleUpdate: (ScaleUpdateDetails details) {
// //缩放
// },
),
);
}
}
  • Tap
    • onTapDown 指针已经在特定位置与屏幕接触
    • onTapUp 指针停止在特定位置与屏幕接触
    • onTap tap事件触发
    • onTapCancel 先前指针触发的onTapDown不会在触发tap事件
  • 双击
    • onDoubleTap 用户快速连续两次在同一位置轻敲屏幕.
  • 长按
    • onLongPress 指针在相同位置长时间保持与屏幕接触
  • 垂直拖动
    • onVerticalDragStart 指针已经与屏幕接触并可能开始垂直移动
    • onVerticalDragUpdate 指针与屏幕接触并已沿垂直方向移动.
    • onVerticalDragEnd 先前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动
  • 水平拖动
    • onHorizontalDragStart 指针已经接触到屏幕并可能开始水平移动
    • onHorizontalDragUpdate 指针与屏幕接触并已沿水平方向移动
    • onHorizontalDragEnd 先前与屏幕接触并水平移动的指针不再与屏幕接触,并在停止接触屏幕时以特定速度移动

GestureRecognizer

GestureDetector 内部是使用一个或多个 GestureRecognizer 来识别各种手势的,而 GestureRecognizer 的作用就是通过 Listener 来将原始指针事件转换为语义手势,GestureDetector 直接可以接收一个子 widget。GestureRecognizer 是一个抽象类,一种手势的识别器对应一个 GestureRecognizer 的子类,Flutter 实现了丰富的手势识别器,我们可以直接使用。

举个栗子

假设我们要给一段富文本(RichText)的不同部分分别添加点击事件处理器,但是 TextSpan 并不是一个 widget,这时我们不能用 GestureDetector,但 TextSpan 有一个 recognizer 属性,它可以接收一个 GestureRecognizer。

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
import 'package:flutter/gestures.dart';

class _GestureRecognizer extends StatefulWidget {
const _GestureRecognizer({Key? key}) : super(key: key);

@override
_GestureRecognizerState createState() => _GestureRecognizerState();
}

class _GestureRecognizerState extends State<_GestureRecognizer> {
TapGestureRecognizer _tapGestureRecognizer = TapGestureRecognizer();
bool _toggle = false; //变色开关

@override
void dispose() {
//用到GestureRecognizer的话一定要调用其dispose方法释放资源
_tapGestureRecognizer.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: Text.rich(
TextSpan(
children: [
TextSpan(text: "你好世界"),
TextSpan(
text: "点我变色",
style: TextStyle(
fontSize: 30.0,
color: _toggle ? Colors.blue : Colors.red,
),
recognizer: _tapGestureRecognizer
..onTap = () {
setState(() {
_toggle = !_toggle;
});
},
),
TextSpan(text: "你好世界"),
],
),
),
);
}
}
  1. 使用 GestureRecognizer 之前,记得导入 import ‘package:flutter/gestures.dart’;
  2. 使用 GestureRecognizer 后一定要调用其 dispose() 方法来释放资源(主要是取消内部的计时器)。

在实际使用时,有很多 Widget 已经对 tap 或手势做了响应。例如 FlatButton 响应 presses,ListView 响应滑动事件触发滚动。