一,概述

  移动应用中一个必不可少的环节就是与用户的交互,在Flutter中提供的手势检测为GestureDetector。 Flutter中的手势系统分为二层:

  • 第一层是触摸原事件(指针)

    • PointerDownEvent:用户与屏幕接触产生了联系
    • PointerMoveEvent:手指已从屏幕上的一个位置移动到另一个位置
    • PointMoveEvent:指针停止接触屏幕
    • PointerUpEvent:用户已停止接触屏幕
    • PointerCanceEvent:此指针的输入不再指向此应用程序
  • 第二层是手势事件(轻击,拖动,缩放)
    • 自带交互的控件监听

      • RaisedButton、
      • IconButton、
      • OutlineButton、
      • Checkbox、
      • SnackBar、
      • Switch等    
    • 不自带交互的控件监听
      • 用GestureDelector进行手势检测
      • 用Dismissible实现滑动删除    

二,手势事件

  • 1.自带交互的控件
    在Flutter中,自带如点击事件的控件有RaisedButton、IconButton、OutlineButton、Checkbox、SnackBar、Switch等,如下面给OutlineButton添加点击事件:

    body:Center(
    child: OutlineButton(
    child: Text('点击我'),
    onPressed: (){
    Fluttertoast.showToast(
    msg: '你点击了FlatButton',
    toastLength: Toast.LENGTH_SHORT,
    gravity: ToastGravity.CENTER,
    timeInSecForIos: ,
    );
    }),
    ),

    上面代码就可以捕捉OutlineButton的点击事件。

  • 2.不自带交互的控件
    • GestureDetector  
      很多控件不像RaisedButton、OutlineButton等已经对presses(taps)或手势做出了响应。那么如果要监听这些控件的手势就需要用另一个控件GestureDetector,那看看源码GestureDetector支持哪些手势:

      GestureDetector({
      Key key,
      this.child,
      this.onTapDown,//按下,每次和屏幕交互都会调用
      this.onTapUp,//抬起,停止触摸时调用
      this.onTap,//点击,短暂触摸屏幕时调用
      this.onTapCancel,//取消 触发了onTapDown,但没有完成onTap
      this.onDoubleTap,//双击,短时间内触摸屏幕两次
      this.onLongPress,//长按,触摸时间超过500ms触发
      this.onLongPressUp,//长按松开
      this.onVerticalDragDown,//触摸点开始和屏幕交互,同时竖直拖动按下
      this.onVerticalDragStart,//触摸点开始在竖直方向拖动开始
      this.onVerticalDragUpdate,//触摸点每次位置改变时,竖直拖动更新
      this.onVerticalDragEnd,//竖直拖动结束
      this.onVerticalDragCancel,//竖直拖动取消
      this.onHorizontalDragDown,//触摸点开始跟屏幕交互,并水平拖动
      this.onHorizontalDragStart,//水平拖动开始,触摸点开始在水平方向移动
      this.onHorizontalDragUpdate,//水平拖动更新,触摸点更新
      this.onHorizontalDragEnd,//水平拖动结束触发
      this.onHorizontalDragCancel,//水平拖动取消 onHorizontalDragDown没有成功触发
      //onPan可以取代onVerticalDrag或者onHorizontalDrag,三者不能并存
      this.onPanDown,//触摸点开始跟屏幕交互时触发
      this.onPanStart,//触摸点开始移动时触发
      this.onPanUpdate,//屏幕上的触摸点位置每次改变时,都会触发这个回调
      this.onPanEnd,//pan操作完成时触发
      this.onPanCancel,//pan操作取消
      //onScale可以取代onVerticalDrag或者onHorizontalDrag,三者不能并存,不能与onPan并存
      this.onScaleStart,//触摸点开始跟屏幕交互时触发,同时会建立一个焦点为1.0
      this.onScaleUpdate,//跟屏幕交互时触发,同时会标示一个新的焦点
      this.onScaleEnd,//触摸点不再跟屏幕交互,标示这个scale手势完成
      this.behavior,
      this.excludeFromSemantics = false
      })

      这里注意:onVerticalXXX/onHorizontalXXX和onPanXXX不能同时设置,如果同时需要水平、竖直方向的移动,设置onPanXXX。
      直接上例子:

      • 2.1.onTapXXX

        child: GestureDetector(
        child: Container(
        width: 300.0,
        height: 300.0,
        color:Colors.red,
        ),
        onTapDown: (d){
        print("onTapDown");
        },
        onTapUp: (d){
        print("onTapUp");
        },
        onTap:(){
        print("onTap");
        },
        onTapCancel: (){
        print("onTaoCancel");
        },
        )

        点了一下,并且抬起,结果是:

        I/flutter (): onTapDown
        I/flutter (): onTapUp
        I/flutter (): onTap

        先触发onTapDown 然后onTapUp 继续onTap

      • 2.2.onLongXXX

        //手势测试
        Widget gestureTest = GestureDetector(
        child: Container(
        width: 300.0,
        height: 300.0,
        color:Colors.red,
        ),
        onDoubleTap: (){
        print("双击onDoubleTap");
        },
        onLongPress: (){
        print("长按onLongPress");
        },
        onLongPressUp: (){
        print("长按抬起onLongPressUP");
        },
        );

        实际结果:

        I/flutter (): 长按onLongPress
        I/flutter (): 长按抬起onLongPressUP
        I/flutter (): 双击onDoubleTap
      • 2.3.onVerticalXXX

        //手势测试
        Widget gestureTest = GestureDetector(
        child: Container(
        width: 300.0,
        height: 300.0,
        color:Colors.red,
        ),
        onVerticalDragDown: (_){
        print("竖直方向拖动按下onVerticalDragDown:"+_.globalPosition.toString());
        },
        onVerticalDragStart: (_){
        print("竖直方向拖动开始onVerticalDragStart"+_.globalPosition.toString());
        },
        onVerticalDragUpdate: (_){
        print("竖直方向拖动更新onVerticalDragUpdate"+_.globalPosition.toString());
        },
        onVerticalDragCancel: (){
        print("竖直方向拖动取消onVerticalDragCancel");
        },
        onVerticalDragEnd: (_){
        print("竖直方向拖动结束onVerticalDragEnd");
        },
        );

        输出结果:

        I/flutter (): 竖直方向拖动按下onVerticalDragDown:Offset(191.7, 289.3)
        I/flutter (): 竖直方向拖动开始onVerticalDragStartOffset(191.7, 289.3)
        I/flutter (): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (): 竖直方向拖动更新onVerticalDragUpdateOffset(191.3, 290.0)
        I/flutter (): 竖直方向拖动更新onVerticalDragUpdateOffset(191.3, 291.3)
        I/flutter (): 竖直方向拖动结束onVerticalDragEnd
      • 2.4.onPanXXX

        //手势测试
        Widget gestureTest = GestureDetector(
        child: Container(
        width: 300.0,
        height: 300.0,
        color:Colors.red,
        ),
        onPanDown: (_){
        print("onPanDown");
        },
        onPanStart: (_){
        print("onPanStart");
        },
        onPanUpdate: (_){
        print("onPanUpdate");
        },
        onPanCancel: (){
        print("onPanCancel");
        },
        onPanEnd: (_){
        print("onPanEnd");
        },
        );

        无论竖直拖动还是横向拖动还是一起来,结果如下:

        I/flutter (): onPanDown
        I/flutter (): onPanStart
        I/flutter (): onPanUpdate
        I/flutter (): onPanUpdate
        I/flutter (): onPanEnd
      • 2.5.onScaleXXX

        //手势测试
        Widget gestureTest = GestureDetector(
        child: Container(
        width: 300.0,
        height: 300.0,
        color:Colors.red,
        ),
        onScaleStart: (_){
        print("onScaleStart");
        },
        onScaleUpdate: (_){
        print("onScaleUpdate");
        },
        onScaleEnd: (_){
        print("onScaleEnd");
        }
        );

        无论点击、竖直拖动、水平拖动,结果如下:

        I/flutter (): onScaleStart
        I/flutter (): onScaleUpdate
        I/flutter (): onScaleUpdate
        I/flutter (): onScaleUpdate
        I/flutter (): onScaleUpdate
        I/flutter (): onScaleUpdate
        I/flutter (): onScaleUpdate
        I/flutter (): onScaleUpdate
        I/flutter (): onScaleEnd
    • 用dismissible实现滑动删除
      滑动删除模式在很多移动应用中很常见。例如,我们在整理手机通讯录时,希望能快速删除一些联系人,一般手指轻轻一滑即可以实现删除功能。Flutter提供了Dismissible组件使这项任务变得简单。

      • 构造函数

        /**
        * 滑动删除
        *
        * const Dismissible({
        @required Key key,//
        @required this.child,//
        this.background,//滑动时组件下一层显示的内容,没有设置secondaryBackground时,从右往左或者从左往右滑动都显示该内容,设置了secondaryBackground后,从左往右滑动显示该内容,从右往左滑动显示secondaryBackground的内容
        //secondaryBackground不能单独设置,只能在已经设置了background后才能设置,从右往左滑动时显示
        this.secondaryBackground,//
        this.onResize,//组件大小改变时回调
        this.onDismissed,//组件消失后回调
        this.direction = DismissDirection.horizontal,//
        this.resizeDuration = const Duration(milliseconds: 300),//组件大小改变的时长
        this.dismissThresholds = const <DismissDirection, double>{},//
        this.movementDuration = const Duration(milliseconds: 200),//组件消失的时长
        this.crossAxisEndOffset = 0.0,//
        })
        */
      • 示例demo

        /***
        * 滑动删除
        */ class MyListView extends StatelessWidget {
        var list = ['第一个','第二个','第三个','第四个','第五个','第六个'];
        @override
        Widget build(BuildContext context) {
        // TODO: implement build
        return new ListView.builder(
        itemCount: list.length,
        itemBuilder: (context,index){
        var item = list[index];
        return new Dismissible(
        key: Key(item),
        child: new ListTile(
        title: new Text(item),
        ), onDismissed: (direction){
        list.remove(index);
        print(direction);
        }, background: Container(
        color: Colors.red,
        child: new Center(
        child: new Text('删除',
        style: new TextStyle(
        color: Colors.white
        )
        ),
        ),
        ),
        secondaryBackground: new Container(
        color: Colors.green,
        ),
        );
        },
        );
        }
        }

三,原始指针事件

  在移动端,各个平台或UI系统的原始指针事件模型基本都是一致,即:一次完整的事件分为三个阶段:手指按下手指移动、和手指抬起,而更高级别的手势(如点击双击拖动等)都是基于这些原始事件的。

  • 响应流程
     当指针按下时,Flutter会对应用程序执行命中测试(Hit Test),以确定指针与屏幕接触的位置存在哪些widget, 指针按下事件(以及该指针的后续事件)然后被分发到由命中测试发现的最内部的widget,然后从那里开始,事件会在widget树中向上冒泡,这些事件会从最内部的widget被分发到widget根的路径上的所有Widget,这和Web开发中浏览器的事件冒泡机制相似, 但是Flutter中没有机制取消或停止冒泡过程,而浏览器的冒泡是可以停止的。
       注意,只有通过命中测试的Widget才能触发事件。
  • 事件监听
         Flutter中可以使用Listener widget来监听原始触摸事件,它也是一个功能性widget。

    Listener({
    Key key,
    this.onPointerDown, //手指按下回调
    this.onPointerMove, //手指移动回调
    this.onPointerUp,//手指抬起回调
    this.onPointerCancel,//触摸事件取消回调
    this.behavior = HitTestBehavior.deferToChild, //在命中测试期间如何表现
    Widget child
    })
    • 示例demo:
      我们先看一个示例,后面再单独讨论一下behavior属性。

      ...
      //定义一个状态,保存当前指针位置
      PointerEvent _event;
      ...
      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(()=>_event=event),
      onPointerMove: (PointerMoveEvent event) => setState(()=>_event=event),
      onPointerUp: (PointerUpEvent event) => setState(()=>_event=event),
      ),
    • 效果图

  

        手指在蓝色矩形区域内移动即可看到当前指针偏移,当触发指针事件时,参数PointerDownEvent、PointerMoveEvent、PointerUpEvent都是PointerEvent的一个子类,PointerEvent类中包括当前指针的一些信息,如:

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

           上面只是PointerEvent一些常用属性,除了这些它还有很多属性,读者可以查看API文档。

    • behavior属性        
      我们重点来介绍一下,它决定子Widget如何响应命中测试,它的值类型为HitTestBehavior,这是一个枚举类,有三个枚举值:
      • deferToChild:子widget会一个接一个的进行命中测试,如果子Widget中有测试通过的,则当前Widget通过,这就意味着,如果指针事件作用于子Widget上时,其父(祖先)Widget也肯定可以收到该事件。

      • opaque:在命中测试时,将当前Widget当成不透明处理(即使本身是透明的),最终的效果相当于当前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 会去子widget判断是否命中测试,而该例中子widget就是 Text("Box A") 。 如果我们想让整个300×150的矩形区域都能点击我们可以将behavior设为HitTestBehavior.opaque。

        注意,该属性并不能用于在Widget树中拦截(忽略)事件,它只是决定命中测试时的Widget大小。

      • translucent:当点击Widget透明区域时,可以对自身边界内及底部可视区域都进行命中测试,这意味着点击顶部widget透明区域时,顶部widget和底部widget都可以接收到事件,例如:

        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范围内非文本区域点击时(顶部Widget透明区域),控制台只会打印“down0”,也就是说顶部widget没有接收到事件,而只有底部接收到了。当放开注释后,再点击时顶部和底部都会接收到事件,此时会打印:
        I/flutter ( ): down1
        I/flutter ( ): down0
        如果behavior值改为HitTestBehavior.opaque,则只会打印"down1"。
  • 忽略PointerEvent

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

    Listener(
    child: AbsorbPointer(
    child: Listener(
    child: Container(
    color: Colors.red,
    width: 200.0,
    height: 100.0,
    ),
    onPointerDown: (event)=>print("in"),
    ),
    ),
    onPointerDown: (event)=>print("up"),
    )

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

【Flutter学习】事件处理与通知之事件处理的更多相关文章

  1. 【Flutter学习】事件处理与通知之通知(Notification)

    一,概述 Notification是Flutter中一个重要的机制,在Widget树中,每一个节点都可以分发通知,通知会沿着当前节点(context)向上传递,所有父节点都可以通过Notificati ...

  2. Flutter学习笔记(32)--PointerEvent事件处理

    如需转载,请注明出处:Flutter学习笔记(32)--PointerEvent事件处理 在Android原生的开发中,对于事件的处理,我们都知道事件分为down.move.up事件,对于ViewGr ...

  3. Flutter学习笔记(35)--通知Notification

    如需转载,请注明出处:Flutter学习笔记(35)--通知Notification 通知的NotificationListener和我们之前写的事件的Listener一样,都是功能性的组件,而且也都 ...

  4. Flutter学习六之实现一个带筛选的列表页面

    上期实现了一个网络轮播图的效果,自定义了一个轮播图组件,继承自StatefulWidget,我们知道Flutter中并没有像Android中activity的概念.页面见的跳转是通过路由从一个全屏组件 ...

  5. Flutter学习笔记(9)--组件Widget

    如需转载,请注明出处:Flutter学习笔记(9)--组件Widget 在Flutter中,所有的显示都是Widget,Widget是一切的基础,我们可以通过修改数据,再用setState设置数据(调 ...

  6. Flutter学习笔记(14)--StatefulWidget简单使用

    如需转载,请注明出处:Flutter学习笔记(14)--StatefulWidget简单使用 今天上班没那么忙,突然想起来我好像没StatefulWidget(有状态组件)的demo,闲来无事,写一个 ...

  7. Flutter学习笔记(16)--Scaffold脚手架、AppBar组件、BottomNavigationBar组件

    如需转载,请注明出处:Flutter学习笔记(15)--MaterialApp应用组件及routes路由详解 今天的内容是Scaffold脚手架.AppBar组件.BottomNavigationBa ...

  8. Flutter学习笔记(25)--ListView实现上拉刷新下拉加载

    如需转载,请注明出处:Flutter学习笔记(25)--ListView实现上拉刷新下拉加载 前面我们有写过ListView的使用:Flutter学习笔记(12)--列表组件,当列表的数据非常多时,需 ...

  9. Flutter学习笔记(27)--数据共享(InheritedWidget)

    如需转载,请注明出处:Flutter学习笔记(27)--数据共享(InheritedWidget) InheritedWidget是Flutter中非常重要的一个功能型组件,它提供了一种数据在widg ...

随机推荐

  1. 日志数据如何同步到MaxCompute

    摘要:日常工作中,企业需要将通过ECS.容器.移动端.开源软件.网站服务.JS等接入的实时日志数据进行应用开发.包括对日志实时查询与分析.采集与消费.数据清洗与流计算.数据仓库对接等场景.本次分享主要 ...

  2. POJ 3525 Most Distant Point from the Sea (半平面交)

    Description The main land of Japan called Honshu is an island surrounded by the sea. In such an isla ...

  3. http 换成 https

    UPDATE SYS_MENU M SET M.href = ( SELECT CASE WHEN substr(N.href, 0, 5) = 'http:' THEN 'https:'||subs ...

  4. vue.js循环语句

    vue.js循环语句 循环使用 v-for 指令. v-for 指令需要以 site in sites 形式的特殊语法, sites 是源数据数组, site 是数组元素迭代的别名. v-for 可以 ...

  5. TCP/IP协议 和 如何实现 互联网上点对点的通信

    1.参考:https://www.cnblogs.com/onepixel/p/7092302.html   TCP/IP 协议采用4层结构,分别是应用层.传输层.网络层 和 链路层   http 属 ...

  6. LG2704 [NOI2001] 炮兵阵地

    题目描述 (试题来源:Link ) 司令部的将军们打算在 \(N\times M\) 的网格地图上部署他们的炮兵部队.一个 \(N\times M\) 的地图由 \(N\) 行 \(M\) 列组成,地 ...

  7. 【c#技术】一篇文章搞掂:Newtonsoft.Json Json.Net

    一.介绍 Json.Net是一个.Net高性能框架. 特点和好处: 1.为.Net对象和JSON之间的转换提供灵活的Json序列化器: 2.为阅读和书写JSON提供LINQ to JSON: 3.高性 ...

  8. appium 链接真机后,运行代码,但是APP并没有启动

    要淡定,链接真机后,问题一下多出来这么多,还没有启动程序,就碰到接二连三的问题. 爽到家了.慢慢解决吧. 具体问题是这样的: # coding=utf-8from appium import webd ...

  9. Hive date_trunc函数

    The function date_trunc is conceptually similar to the trunc function for numbers. date_trunc('field ...

  10. Flink水印机制(watermark)

    Flink流处理时间方式 EventTime 时间发生的时间,例如:点击网站上的某个链接的时间 IngestionTime 某个Flink节点的source operator接收到数据的时间,例如:某 ...