flutter用户交互事件处理

在移动端所谓的用户交互事件既是用户的手势操作处理。
手势操作在flutter中可分为两类:

  • 第一类是原始的指针事件(Pointer Event),即原生开发中常见的触摸事件,表示屏幕上触摸(或鼠标、手写笔)行为触发所的位移行为。
  • 第二类则是手势识别(Gesture Detector),表示多个原生指针事件的组合操作,如点击、双击、长按、等,是指针事件的语义化封装。

指针事件

在移动端,各个平台或UI系统的原始指针事件模型基本都是一致,即:一次完整的事件分为三个阶段:手指按下(PointerDownEvent)、手指移动(PointerMoveEvent)、和手指抬起(PointerUpEvent),还有触摸取消事件(PointerCancelEvent)。

从官方文档中我们可以了解到:指针按下事件(以及该指针的后续事件)然后被分发到由命中测试发现的最内部的组件,然后从那里开始,事件会在组件树中向上冒泡,这些事件会从最内部的组件被分发到组件树根的路径上的所有组件。

也就是说flutter的指针事件机制也是基于冒泡事件进行处理的,但是这种冒泡却无法自主停止。只能通过命中测试(Hit Test)去调整组件在生命中测试期内该如何表现,比如把触摸事件交给子组件,或者交给其他视图层级之下的组件去响应。

Flutter中可以使用Listener来监听原始触摸事件,按照本书对组件的分类,则Listener也是一个功能性组件。下面是Listener的构造函数定义:

Listener(
  child: Container(
    alignment: Alignment.center,
    color: Colors.blue,
    width: 300.0,
    height: 150.0,
    child: Text(_event?.toString()??"",style: TextStyle(color: Colors.white)),
  ),
  onPointerDown: (PointerDownEvent event) => setState(()=> print("down $event"),//手势按下回调
  onPointerMove: (PointerMoveEvent event) => setState(()=>print("down $event"),//手势移动回调
  onPointerUp: (PointerUpEvent event) => setState(()=>print("down $event"),//手势抬起回调
),

通过拖拽绿色区块可以看到控制台打印如下:

I/flutter (13829): up PointerUpEvent(Offset(97.7, 287.7))
I/flutter (13829): down PointerDownEvent(Offset(110.7, 317.7))
I/flutter (13829): move PointerMoveEvent(Offset(112.9, 317.7))
I/flutter (13829): move PointerMoveEvent(Offset(114.2, 317.7))
I/flutter (13829): up PointerUpEvent(Offset(117.1, 315.7))

上面说到的命中测试是通过PointerEvent中的 behavior 属性控制的。
改属性的值类型为HitTestBehavior,这是一个枚举类,有三个枚举值:

  • deferToChild:子组件会一个接一个的进行命中测试,如果子组件中有测试通过的,则当前组件通过,这就意味着,如果指针事件作用于子组件上时,其父级组件也肯定可以收到该事件。
  • opaque:在命中测试时,将当前组件当成不透明处理(即使本身是透明的),最终的效果相当于当前Widget的整个区域都是点击区域。举个例子:
Listener(
    child: ConstrainedBox(
        constraints: BoxConstraints.tight(Size(300.0, 150.0)),
        child: Center(child: Text("Box A")),
    ),
    //behavior: HitTestBehavior.opaque,
    onPointerDown: (event) => print("down A")
),

上例中,只有点击文本内容区域才会触发点击事件,因为 deferToChild 会去子组件判断是否命中测试,而该例中子组件就是 Text("Box A") 。 如果我们想让整个300×150的矩形区域都能点击我们可以将behavior设为HitTestBehavior.opaque。注意,该属性并不能用于在组件树中拦截(忽略)事件,它只是决定命中测试时的组件大小。

  • translucent:当点击组件透明区域时,可以对自身边界内及底部可视区域都进行命中测试,这意味着点击顶部组件透明区域时,顶部组件和底部组件都可以接收到事件,例如:
Stack(
  children: <Widget>[
    Listener(
      child: ConstrainedBox(
        constraints: BoxConstraints.tight(Size(300.0, 200.0)),
        child: DecoratedBox(
            decoration: BoxDecoration(color: Colors.blue)),
      ),
      onPointerDown: (event) => print("down0"),
    ),
    Listener(
      child: ConstrainedBox(
        constraints: BoxConstraints.tight(Size(200.0, 100.0)),
        child: Center(child: Text("左上角200*100范围内非文本区域点击")),
      ),
      onPointerDown: (event) => print("down1"),
      //behavior: HitTestBehavior.translucent, //放开此行注释后可以"点透"
    )
  ],
)

上例中,当注释掉最后一行代码后,在左上角200*100范围内非文本区域点击时(顶部组件透明区域),控制台只会打印“down0”,也就是说顶部组件没有接收到事件,而只有底部接收到了。当放开注释后,再点击时顶部和底部都会接收到事件,此时会打印:
I/flutter ( 3039): down1
I/flutter ( 3039): down0
如果behavior值改为HitTestBehavior.opaque,则只会打印"down1"。

手势识别

flutter 是通过GestureDetector和GestureRecognizer处理手势。

GestureDetector 常用的方法:
  • onTap:点击
  • onDoubleTap:双击
  • onLongPress:长按
  • onPanUpdate:拖拽
  • onScaleUpdate:缩放
  • 等等
    flutter支持手势监听多个事件,也就是说可以同时监听多个事件。但是由于有手势竞技场(下面解释)出现,因此每次只有一个事件能竞技成功也就是能得到相应。

如下例子所示:假设有一个widget,它可以左右拖动,现在我们也想检测在它上面手指按下和抬起的事件,代码如下:

class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
  double _left = 0.0;
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          left: _left,
          child: GestureDetector(
              child: CircleAvatar(child: Text("B")), //要拖动和点击的widget
              onHorizontalDragUpdate: (DragUpdateDetails details) {
                setState(() {
                  _left += details.delta.dx;
                });
              },
              onHorizontalDragEnd: (details){
                print("onHorizontalDragEnd");
              },
              onTapDown: (details){
                print("down");
              },
              onTapUp: (details){
                print("up");
              },
          ),
        )
      ],
    );
  }
}

现在我们按住圆形“B”拖动然后抬起手指,控制台日志如下:

I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd

具体原因:刚开始按下手指时在没有移动时,拖动手势还没有完整的语义,此时TapDown手势胜出(win),此时打印"down",而拖动时,拖动手势会胜出,当手指抬起时,onHorizontalDragEnd 和 onTapUp发生了冲突,但是因为是在拖动的语义中,所以onHorizontalDragEnd胜出,所以就会打印 “onHorizontalDragEnd”。
当然如果只是单纯想要得到按下,抬起,以及拖拽触发,按下和抬起这种单一事件是可以通过结合Listener指针事件进行处理。

Positioned(
  top:80.0,
  left: _leftB,
  child: Listener(
    onPointerDown: (details) {
      print("down");
    },
    onPointerUp: (details) {
      //会触发
      print("up");
    },
    child: GestureDetector(
      child: CircleAvatar(child: Text("C")),
      onHorizontalDragUpdate: (DragUpdateDetails details) {
        setState(() {
          _leftB += details.delta.dx;
        });
      },
      onHorizontalDragEnd: (details) {
        print("onHorizontalDragEnd");
      },
    ),
  ),
)

如上写法:可以监听到用户点击,拖拽,以及抬起事件。

手势竞技场

其实现方式是通过GestureDetector内部对每一个手势都建立了一个工厂类(Gesture Factory)。而工厂类内部都会使用手势识别类(GestureRecognizer),来确定当前处理的手势。

而所有手势的工厂类都会交给RawGestureDetector类,以完成检测手势大量工作:使用Listener监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器。然后这些手势会在竞技场决定最后又谁来响应用户事件。

但现实场景中也存在当点击时需要父类事件和子类事件同时执行响应的操作。如下代码:

GestureDetector(
   onTap: ()=> print('parent tap');
   child:Container(
    color:Colors.pinkAccent,
    child: GestureDetector(
    onTap: ()=> print('child tap');
    child:Container(
      color:Colors.red,
      width:200.0,
      height:200.0,
      ),
    )               
  ),
);

运行后点击蓝色区域,可以发现只打印了 Child tap;
因此为了能让父类也能接收到手势,我们需要同时使用RawGestureGetector和GestureFactory,来改变竞技场决定谁来响应事件的结果。

① 我们需要自定义一个手势识别器,让改手势识别器在竞技失败时也能够把自己重新添加回来,以便接下来能继续去响应用户事件。

如下定义了一个继承点击手势识别器TapGestureRecognizer的类并重写了器rejectGesture方法,手动的把自己复活:

class MultipleTapGesture extends TapGestureRecognizer{
  @override
  void rejectGesture(int pointer) {
    acceptGesture(pointer);
  }
}

② 需要将手势识别器和其工厂类传递给RawGestureDetector,以便用户产生手势交互事件时能够立刻找到对应的识别方法。RawGestureDetector的初始化函数所做的配置工作,就是定义手势识别器和其工厂类的映射关系。

如下所示:点击蓝色区域时,其父类容器也收到了Tap事件

RawGestureDetector(// 自己构造父 Widget 的手势识别映射关系
  gestures: {
    // 建立多手势识别器与手势识别工厂类的映射关系,从而返回可以响应该手势的 recognizer
    MultipleTapGesture: GestureRecognizerFactoryWithHandlers<
        MultipleTapGesture>(
          () => MultipleTapGesture(),
          (MultipleTapGesture instance) {
        instance.onTap = () => print('parent tapped ');// 点击回调
      },
    )
  },
  child: Container(
    color: Colors.pinkAccent,
    child: Center(
      child: GestureDetector(// 子视图可以继续使用 GestureDetector
        onTap: () => print('Child tapped'),
        child: Container(...),
      ),
    ),
  ),
);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 前言 在 Flutter 中手势操作分为两类: 原始的指针事件(Pointer Event),即原生开发中常见的触...
    Eren丶耶格尔阅读 2,483评论 6 3
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,995评论 4 26
  • 本文主要讲解iOS触摸事件的一系列机制,涉及的问题大致包括: 触摸事件由触屏生成后如何传递到当前应用? 应用接收触...
    baihualinxin阅读 1,203评论 0 9
  • 作者: Mike Bluestein | 译:孙印凤 原文地址: [https://www.smashingmag...
    格老子阅读 3,464评论 0 6
  • 我绝不会停下书写的手。岁月更迭,悲欢交织,命运跌打,令我早已深深懂得了什么是生命中最珍贵的东西。 有时候,只要一两...
    矮马阅读 484评论 3 12