一,概述

  移动应用中一个必不可少的环节就是与用户的交互,在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. DOS基础使用专题(强烈推荐)2

    DOS下硬件设备的使用与设置 由于电脑的普及和应用的日益深入,为了满足人们的需要,电脑的功能随着它的发展变得越来越强大,硬件设备也越来越多,如从原来的ISA及PCI声卡.调制解调器等到现在的USB硬盘 ...

  2. 树结构遍历节点名字提取,这里提取的是el-tree数据结构,封装成函数

    /** * 树形数据提取节点 * @param {*} data */ export function treeDataGetnode (data) { var res = [] var child= ...

  3. 结构体和typedef

    在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据.结构体的定义形式为: struct 结构体名{    结构体所包含的变量或数组}; 结构体是一种集合,它里面包含了多个变量或数组,它 ...

  4. COALESCE 函数作用

    用途. 将空值替换成其他值 返回第一个非空值. 任意一个不为空的值.比较有用.

  5. Python3学习(18)--偏函数(Partial) --转载存地址

    由于最近出差,没有时间更,  倒不是忙,而是费心,项目其实并不难,主要是涉及钱的地方谈技术略显苍白:没有技术解决不了的问题,但是钱没到位,没人愿意无偿给你提供技术,算是停更一周后的,吐槽吧. 赶上今天 ...

  6. UVA10271_Chopsticks

    Chopsticks 大致就是有一堆筷子,知道了他们的长度,现在选长度为abc的三个筷子a<=b<=c为一对筷子,质量为(a-b)平方,现在选k双这样的筷子,求质量最小 思路: 第一次看到 ...

  7. Intellij IDEA 智能补全的 10 个姿势,简直不能太牛逼!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 一年多前,栈长那时候刚从 Eclipse 转型 IDEA 成功,前面转了好多次,都是失败史,都是泪.. 后面我就在微信公众号 ...

  8. 安装Consul服务中心

    安装Consul服务中心 首先下载对应版本的安装程序.点击下载 我下载的是macOS64位版本,下载文件是一个ZIP文件,下载后解压缩到一个你喜欢的位置,以开发模式启动consul服务: #进入con ...

  9. shell数学运算

    shell的数学运算 branches@ubuntu:~$ var1=$[ * ] branches@ubuntu:~$ echo $var1 branches@ubuntu:~$ var2=$[$v ...

  10. 2018-2-13-win10-UWP-单元测试

    title author date CreateTime categories win10 UWP 单元测试 lindexi 2018-2-13 17:23:3 +0800 2018-2-13 17: ...