跳到正文
Willh的个人博客 记录 · 思考 · 分享
《Flutter实战·第二版》

8.2 手势识别

本节先介绍一些Flutter中用于处理手势的GestureDetector和GestureRecognizer,然后再仔细讨论一下手势竞争与冲突问题。

8.2.1 GestureDetector

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

点击、双击、长按

我们通过GestureDetector对Container进行手势识别,触发相应事件后,在Container上显示事件名,为了增大点击区域,将Container设置为200×100,代码如下:

1class _GestureTestState extends State<GestureTest> {
2  String _operation = "No Gesture detected!"; //保存事件名
3  
4  Widget build(BuildContext context) {
5    return Center(
6      child: GestureDetector(
7        child: Container(
8          alignment: Alignment.center,
9          color: Colors.blue,
10          width: 200.0,
11          height: 100.0,
12          child: Text(
13            _operation,
14            style: TextStyle(color: Colors.white),
15          ),
16        ),
17        onTap: () => updateText("Tap"), //点击
18        onDoubleTap: () => updateText("DoubleTap"), //双击
19        onLongPress: () => updateText("LongPress"), //长按
20      ),
21    );
22  }
23
24  void updateText(String text) {
25    //更新显示的事件名
26    setState(() {
27      _operation = text;
28    });
29  }
30}

运行效果如图8-2所示:

图8-2

注意: 当同时监听onTap和onDoubleTap事件时,当用户触发tap事件时,会有200毫秒左右的延时,这是因为当用户点击完之后很可能会再次点击以触发双击事件,所以GestureDetector会等一段时间来确定是否为双击事件。如果用户只监听了onTap(没有监听onDoubleTap)事件时,则没有延时。

拖动、滑动

一次完整的手势过程是指用户手指按下到抬起的整个过程,期间,用户按下手指后可能会移动,也可能不会移动。GestureDetector对于拖动和滑动事件是没有区分的,他们本质上是一样的。GestureDetector会将要监听的组件的原点(左上角)作为本次手势的原点,当用户在监听的组件上按下手指时,手势识别就会开始。下面我们看一个拖动圆形字母A的示例:

1class _Drag extends StatefulWidget {
2  
3  _DragState createState() => _DragState();
4}
5
6class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
7  double _top = 0.0; //距顶部的偏移
8  double _left = 0.0;//距左边的偏移
9
10  
11  Widget build(BuildContext context) {
12    return Stack(
13      children: <Widget>[
14        Positioned(
15          top: _top,
16          left: _left,
17          child: GestureDetector(
18            child: CircleAvatar(child: Text("A")),
19            //手指按下时会触发此回调
20            onPanDown: (DragDownDetails e) {
21              //打印手指按下的位置(相对于屏幕)
22              print("用户手指按下:${e.globalPosition}");
23            },
24            //手指滑动时会触发此回调
25            onPanUpdate: (DragUpdateDetails e) {
26              //用户手指滑动时,更新偏移,重新构建
27              setState(() {
28                _left += e.delta.dx;
29                _top += e.delta.dy;
30              });
31            },
32            onPanEnd: (DragEndDetails e){
33              //打印滑动结束时在x、y轴上的速度
34              print(e.velocity);
35            },
36          ),
37        )
38      ],
39    );
40  }
41}

运行后,就可以在任意方向拖动了,运行效果如图8-3所示:

图8-3

日志:

1I/flutter ( 8513): 用户手指按下:Offset(26.3, 101.8)
2I/flutter ( 8513): Velocity(235.5, 125.8)

代码解释:

  • DragDownDetails.globalPosition:当用户按下时,此属性为用户按下的位置相对于屏幕(而非父组件)原点(左上角)的偏移。
  • DragUpdateDetails.delta:当用户在屏幕上滑动时,会触发多次Update事件,delta指一次Update事件的滑动的偏移量。
  • DragEndDetails.velocity:该属性代表用户抬起手指时的滑动速度(包含x、y两个轴的),示例中并没有处理手指抬起时的速度,常见的效果是根据用户抬起手指时的速度做一个减速动画。

单一方向拖动

在本示例中,是可以朝任意方向拖动的,但是在很多场景,我们只需要沿一个方向来拖动,如一个垂直方向的列表,GestureDetector可以只识别特定方向的手势事件,我们将上面的例子改为只能沿垂直方向拖动:

1class _DragVertical extends StatefulWidget {
2  
3  _DragVerticalState createState() => _DragVerticalState();
4}
5
6class _DragVerticalState extends State<_DragVertical> {
7  double _top = 0.0;
8
9  
10  Widget build(BuildContext context) {
11    return Stack(
12      children: <Widget>[
13        Positioned(
14          top: _top,
15          child: GestureDetector(
16            child: CircleAvatar(child: Text("A")),
17            //垂直方向拖动事件
18            onVerticalDragUpdate: (DragUpdateDetails details) {
19              setState(() {
20                _top += details.delta.dy;
21              });
22            },
23          ),
24        )
25      ],
26    );
27  }
28}

这样就只能在垂直方向拖动了,如果只想在水平方向滑动同理。

缩放

GestureDetector可以监听缩放事件,下面示例演示了一个简单的图片缩放效果:

1class _Scale extends StatefulWidget {
2  const _Scale({Key? key}) : super(key: key);
3
4  
5  _ScaleState createState() => _ScaleState();
6}
7
8class _ScaleState extends State<_Scale> {
9  double _width = 200.0; //通过修改图片宽度来达到缩放效果
10
11  
12  Widget build(BuildContext context) {
13    return Center(
14      child: GestureDetector(
15        //指定宽度,高度自适应
16        child: Image.asset("./images/sea.png", width: _width),
17        onScaleUpdate: (ScaleUpdateDetails details) {
18          setState(() {
19            //缩放倍数在0.8到10倍之间
20            _width=200*details.scale.clamp(.8, 10.0);
21          });
22        },
23      ),
24    );
25  }
26}

运行效果如图8-4所示:

图8-4

现在在图片上双指张开、收缩就可以放大、缩小图片。本示例比较简单,实际中我们通常还需要一些其它功能,如双击放大或缩小一定倍数、双指张开离开屏幕时执行一个减速放大动画等,读者可以在学习完后面“动画”一章中的内容后自己来尝试实现一下。

8.2.2 GestureRecognizer

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

示例

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

假设我们需要在点击时给文本变色:

1import 'package:flutter/gestures.dart';
2
3class _GestureRecognizer extends StatefulWidget {
4  const _GestureRecognizer({Key? key}) : super(key: key);
5
6  
7  _GestureRecognizerState createState() => _GestureRecognizerState();
8}
9
10class _GestureRecognizerState extends State<_GestureRecognizer> {
11  TapGestureRecognizer _tapGestureRecognizer = TapGestureRecognizer();
12  bool _toggle = false; //变色开关
13
14  
15  void dispose() {
16    //用到GestureRecognizer的话一定要调用其dispose方法释放资源
17    _tapGestureRecognizer.dispose();
18    super.dispose();
19  }
20
21  
22  Widget build(BuildContext context) {
23    return Center(
24      child: Text.rich(
25        TextSpan(
26          children: [
27            TextSpan(text: "你好世界"),
28            TextSpan(
29              text: "点我变色",
30              style: TextStyle(
31                fontSize: 30.0,
32                color: _toggle ? Colors.blue : Colors.red,
33              ),
34              recognizer: _tapGestureRecognizer
35                ..onTap = () {
36                  setState(() {
37                    _toggle = !_toggle;
38                  });
39                },
40            ),
41            TextSpan(text: "你好世界"),
42          ],
43        ),
44      ),
45    );
46  }
47}

运行效果:

图8-5

注意:使用GestureRecognizer后一定要调用其dispose()方法来释放资源(主要是取消内部的计时器)。