文章

02.Flutter事件

02.Flutter事件

Flutter 事件基础

Flutter 原始事件

Listener 组件

Flutter 中可以使用 Listener 来监听原始触摸事件

1
2
3
4
5
6
7
8
9
Listener({
  Key key,
  this.onPointerDown, // 手指按下回调
  this.onPointerMove, // 手指移动回调
  this.onPointerUp, // 手指抬起回调
  this.onPointerCancel, // 触摸事件取消回调
  this.behavior = HitTestBehavior.deferToChild, // 先忽略此参数,后面小节会专门介绍
  Widget child
})

参数 PointerDownEvent、 PointerMoveEvent、 PointerUpEvent 都是 PointerEvent 的子类,PointerEvent 类中包括当前指针的一些信息,注意 Pointer,即 “ 指针 “, 指事件的触发者,可以是鼠标、触摸板、手指:

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

忽略指针事件

不想让某个子树响应 PointerEvent 的话,我们可以使用 IgnorePointerAbsorbPointer,这两个组件都能阻止子树接收指针事件,不同之处在于 AbsorbPointer 本身会参与命中测试,而 IgnorePointer 本身不会参与,这就意味着 AbsorbPointer 本身是可以接收指针事件的 (但其子树不行),而 IgnorePointer 不可以

1
2
3
4
5
6
7
8
9
10
11
12
13
Listener(
  child: AbsorbPointer(
    child: Listener(
      child: Container(
        color: Colors.red,
        width: 200.0,
        height: 100.0,
      ),
      onPointerDown: (event) => print("in ${DateTime.timestamp()}"),
    ),
  ),
  onPointerDown: (event) => print("up ${DateTime.timestamp()}"),
)

点击 Container 时,由于它在 AbsorbPointer 的子树上,所以不会响应指针事件,所以日志不会输出 “in”,但 AbsorbPointer 本身是可以接收指针事件的,所以会输出 “up”。如果将 AbsorbPointer 换成 IgnorePointer,那么两个都不会输出。

手势识别

Flutter 中用于处理手势的 GestureDetectorGestureRecognizer

GestureDetector

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

点击、双击、长按

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
class GestureTest extends StatefulWidget {
  const GestureTest({super.key});

  @override
  State<StatefulWidget> createState() {
    return _GestureTestState();
  }
}

class _GestureTestState extends State<GestureTest> {
  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;
    });
  }
}
  • onTap 点击
  • onDoubleTap 双击
  • onLongPress 长按
  • 当同时监听 onTap 和 onDoubleTap 事件时,当用户触发 tap 事件时,会有 200 毫秒左右的延时,这是因为当用户点击完之后很可能会再次点击以触发双击事件,所以 GestureDetector 会等一段时间来确定是否为双击事件。如果用户只监听了 onTap(没有监听 onDoubleTap)事件时,则没有延时

拖动、滑动

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
class _Drag extends StatefulWidget {
  @override
  _DragState createState() => _DragState();
}

class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
  double _top = 0.0; //距顶部的偏移
  double _left = 0.0;//距左边的偏移

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        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);
            },
          ),
        )
      ],
    );
  }
}
  • DragDownDetails.globalPosition:当用户按下时,此属性为用户按下的位置相对于屏幕(而非父组件)原点 (左上角) 的偏移。
  • DragUpdateDetails.delta:当用户在屏幕上滑动时,会触发多次 Update 事件,delta 指一次 Update 事件的滑动的偏移量
  • DragEndDetails.velocity:该属性代表用户抬起手指时的滑动速度 (包含 x、y 两个轴的),示例中并没有处理手指抬起时的速度,常见的效果是根据用户抬起手指时的速度做一个减速动画
单一方向拖动
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
class _DragVertical extends StatefulWidget {
  @override
  _DragVerticalState createState() => _DragVerticalState();
}

class _DragVerticalState extends State<_DragVertical> {
  double _top = 0.0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //垂直方向拖动事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            },
          ),
        )
      ],
    );
  }
}
  • onVerticalDragUpdate

缩放

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
class _Scale extends StatefulWidget {
  const _Scale({Key? key}) : super(key: key);

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

class _ScaleState extends State<_Scale> {
  double _width = 200.0; //通过修改图片宽度来达到缩放效果

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        //指定宽度,高度自适应
        child: Image.asset("./images/sea.png", width: _width),
        onScaleUpdate: (ScaleUpdateDetails details) {
          setState(() {
            //缩放倍数在0.8到10倍之间
            _width=200*details.scale.clamp(.8, 10.0);
          });
        },
      ),
    );
  }
}

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
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: "你好世界"),
          ],
        ),
      ),
    );
  }
}

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

手势和触摸事件处理

widget 添加点击事件

  1. widget 支持事件监听,直接传递给它一个函数,并在这个函数里实现响应方法
1
2
3
4
5
6
Widget build(BuildContext context) {
  return RaisedButton(
    onPressed: () { print('click') },
    child: Text('Button')
  );
}
  1. widget 本身不支持事件监听,则在这个 widget 外面包裹一个 GestureDetector,并给它的 onTap 属性传递一个参数

处理 widget 上的其他手势

使用 GestureDetector,可以监听多种手势:

  • 点击
    • onTapDown 在特定位置轻触了屏幕
    • onTapUp 在特定位置产生了一个轻触手机,并停止接触屏幕
    • onTap 产生了一个轻触手势
    • onTapCancel 触发了 onTapDown 但没能触发 tap
  • 双击
    • onDoubleTap 用户在同一个位置快速点击了两下屏幕
  • 长按
    • onLongPress 用户在同一个位置长时间接触屏幕
  • 垂直拖动
    • onVerticalDragStart 接触了屏幕,并且可能会垂直移动
    • onVerticalDragUpdate 接触了屏幕,并继续在垂直方向移动
    • onVerticalDragEnd 之前接触了屏幕并垂直移动,并在停止接触屏幕前以某个垂直的速度移动
  • 水平拖动
    • onHorizontalDragStart 接触了屏幕,并且可能会水平移动
    • onHorizontalDragUpdate 接触了屏幕,并继续在水平方向移动
    • onHorizontalDragEnd 之前接触了屏幕并水平移动,并在停止接触屏幕前以某个水平的速度移动

Flutter 事件机制

Flutter 事件处理流程

本文由作者按照 CC BY 4.0 进行授权