Flutter事件监听

一. 事件监听

在大前端的开发中,必然存在各种各样和用户交互的情况:比如手指点击、手指滑动、双击、长按等等。

在Flutter中,手势有两个不同的层次:

  • 第一层:原始指针事件(Pointer Events):描述了屏幕上由触摸板、鼠标、指示笔等触发的指针的位置和移动。
  • 第二层:手势识别(Gesture Detector):这个是在原始事件上的一种封装。
    • 比如我们要监听用户长按,如果自己封装原始事件我们需要监听从用户按下到抬起的时间来判断是否是一次长按事件;
    • 比如我们需要监听用户双击事件,我们需要自己封装监听用户两次按下抬起的时间间隔;
    • 幸运的是各个平台几乎都对它们进行了封装,而Flutter中的手势识别就是对原始指针事件的封装;
    • 包括哪些手势呢?比如点击、双击、长按、拖动等。

2.1. 指针事件Pointer

Pointer 代表的是人机界面交互的原始数据。一共有四种指针事件:

Pointer的原理是什么呢?

  • 在指针落下时,框架做了一个 hit test 的操作,确定与屏幕发生接触的位置上有哪些Widget以及分发给最内部的组件去响应;
  • 事件会沿着最内部的组件向组件树的根冒泡分发;
  • 并且不存在用于取消或者停止指针事件进一步分发的机制;

原始指针事件使用Listener来监听:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Listener(
        child: Container(
          width: 200,
          height: 200,
          color: Colors.red,
        ),
        onPointerDown: (event) => print("手指按下:$event"),
        onPointerMove: (event) => print("手指移动:$event"),
        onPointerUp: (event) => print("手指抬起:$event"),
      ),
    );
  }
}

如果想拿到点击的位置,可以用如下方式:

// 拿到相当于屏幕坐标位置
print(event.position);
// 拿到相当于红色矩形框的坐标位置
print(event.localPosition);

2.2. 手势识别Gesture

Gesture是对一系列Pointer的封装,官方建议开发中尽可能使用Gesture,而不是Pointer。

Gesture分成非常多的种类:

点击

  • onTapDown:用户发生手指按下的操作
  • onTapUp:用户发生手指抬起的操作
  • onTap:用户点击事件完成
  • onTapCancel:事件按下过程中被取消

双击:

  • onDoubleTap:快速点击了两次

长按:

  • onLongPress:在屏幕上保持了一段时间

纵向拖拽:

  • onVerticalDragStart:指针和屏幕产生接触并可能开始纵向移动;
  • onVerticalDragUpdate:指针和屏幕产生接触,在纵向上发生移动并保持移动;
  • onVerticalDragEnd:指针和屏幕产生接触结束;

横向拖拽:

  • onHorizontalDragStart:指针和屏幕产生接触并可能开始横向移动;
  • onHorizontalDragUpdate:指针和屏幕产生接触,在横向上发生移动并保持移动;
  • onHorizontalDragEnd:指针和屏幕产生接触结束;

移动:

  • onPanStart:指针和屏幕产生接触并可能开始横向移动或者纵向移动。如果设置了 onHorizontalDragStart 或者 onVerticalDragStart,该回调方法会引发崩溃;
  • onPanUpdate:指针和屏幕产生接触,在横向或者纵向上发生移动并保持移动。如果设置了 onHorizontalDragUpdate 或者 onVerticalDragUpdate,该回调方法会引发崩溃;
  • onPanEnd:指针先前和屏幕产生了接触,并且以特定速度移动,此后不再在屏幕接触上发生移动。如果设置了 onHorizontalDragEnd 或者 onVerticalDragEnd,该回调方法会引发崩溃;

从Widget的层面来监听手势,我们需要使用:GestureDetector。

  • 当然,我们也可以使用RaisedButton、FlatButton、InkWell等来监听手势
  • globalPosition用于获取相对于屏幕的位置信息
  • localPosition用于获取相对于当前Widget的位置信息
class GestureDemo extends StatelessWidget {
  const GestureDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTapDown: (details) {
          print("手指按下");
          print(details.globalPosition);
          print(details.localPosition);
        },
        onTapUp: (details) {
          print("手指抬起");
        },
        onTapCancel: () {
          print("手势取消");
        },
        onTap: () {
          print("手势点击");
        },
        onDoubleTap: () {
          print("手指双击");
        },
        onLongPress: () {
          print("长按手势");
        },
        child: Container(
          width: 200,
          height: 200,
          color: Colors.orange,
        ),
      ),
    );
  }
}

2.3. 阻止冒泡

  1. 当Container里面包裹另外一个Container的时候,就算里面的Container也设置宽高,里面的Container的宽高也会撑满外面的Container。如果我们想让里面的Container有宽高,可以将外面的Container改成column,或者给外面的Container添加alignment属性

  2. 默认情况下,点击里面的Container的事件偶尔的时候会冒泡到外面,如果我们不想冒泡到外面,可以使用Stack让两个Container层叠起来,这时候我们点击里面的就会打印inner click,点击外面的就会打印outer click

  3. 如果我们想无论点击里面还是外面都打印outer click,可以使用IgnorePointer忽略里面的点击

class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("列表测试"),
      ),
      body: Center(
        child: Stack(
          alignment: Alignment.center,
          children: <Widget>[
            GestureDetector(
              onTapDown: (details) {
                print("outer click");
              },
              child: Container(
                width: 200,
                height: 200,
                color: Colors.yellow,
              // 当Container里面包裹另外一个Container的时候,就算里面的Container也设置宽高,里面的Container的宽高也会撑满外面的Container
              // 如果我们想让里面的Container有宽高,可以将外面的Container改成column,或者给外面的Container添加alignment属性
                alignment: Alignment.center,
              ),
            ),
            // 默认情况下,点击里面的Container的事件偶尔的时候会冒泡到外面,如果我们不想冒泡到外面,可以使用Stack让两个Container层叠起来,这时候我们点击里面的就会打印inner click,点击外面的就会打印outer click
            // 如果我们想无论点击里面还是外面都打印outer click,可以使用IgnorePointer忽略里面的点击
            IgnorePointer(
              child: GestureDetector(
                onTapDown: (details) {
                  print("inner click");
                },
                child: Container(
                  width: 100,
                  height: 100,
                  color: Colors.red,
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

二. 跨组件事件:EventBus

在组件之间如果有事件需要传递,一方面可以一层层来传递,另一方面我们也可以使用一个EventBus工具来完成。

其实EventBus在Vue、React中都是一种非常常见的跨组件通信的方式:

  • EventBus相当于是一种订阅者模式,通过一个全局的对象来管理;
  • 这个EventBus我们可以自己实现,也可以使用第三方的EventBus;

这里我们直接选择第三方的EventBus:

dependencies:
  event_bus: ^1.1.1

第一:我们需要定义一个希望在组件之间传递的对象

  • 我们可以称之为一个事件对象,也可以是我们平时开发中用的模型对象(model)
class UserInfo {
  String nickname;
  int level;

  UserInfo(this.nickname, this.level);
}

第二:创建一个全局的EventBus对象

final eventBus = EventBus();

第三:在某个Widget中,发出事件

class HYButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: Text("HYButton"),
      onPressed: () {
        final info = UserInfo("why", 18);
        eventBus.fire(info);
      },
    );
  }
}

第四:在某个Widget中,监听事件

class HYText extends StatefulWidget {
  @override
  _HYTextState createState() => _HYTextState();
}

class _HYTextState extends State<HYText> {
  String message = "Hello Coderwhy";

  @override
  void initState() {
    super.initState();

    eventBus.on<UserInfo>().listen((data) {
      setState(() {
        message = "${data.nickname}-${data.level}";
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text(message, style: TextStyle(fontSize: 30),);
  }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容