zoukankan      html  css  js  c++  java
  • 【Flutter学习】事件处理与通知之事件处理

    一,概述

      移动应用中一个必不可少的环节就是与用户的交互,在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: 1,
           );
        }),
      ),

      上面代码就可以捕捉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(
                       300.0,
                     height: 300.0,
                      color:Colors.red,
              ),
            onTapDown: (d){
               print("onTapDown");
             },
            onTapUp: (d){
               print("onTapUp");
             },
            onTap:(){
               print("onTap");
             },
            onTapCancel: (){
                print("onTaoCancel");
             },
          )

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

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

          先触发onTapDown 然后onTapUp 继续onTap

        • 2.2.onLongXXX

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

          实际结果:

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

          //手势测试
          Widget gestureTest = GestureDetector(
          child: Container(
           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 (16304): 竖直方向拖动按下onVerticalDragDown:Offset(191.7, 289.3)
          I/flutter (16304): 竖直方向拖动开始onVerticalDragStartOffset(191.7, 289.3)
          I/flutter (16304): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
          I/flutter (16304): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
          I/flutter (16304): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
          I/flutter (16304): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
          I/flutter (16304): 竖直方向拖动更新onVerticalDragUpdateOffset(191.7, 289.3)
          I/flutter (16304): 竖直方向拖动更新onVerticalDragUpdateOffset(191.3, 290.0)
          I/flutter (16304): 竖直方向拖动更新onVerticalDragUpdateOffset(191.3, 291.3)
          I/flutter (16304): 竖直方向拖动结束onVerticalDragEnd
        • 2.4.onPanXXX

          //手势测试
          Widget gestureTest = GestureDetector(
           child: Container(
             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 (16304): onPanDown
          I/flutter (16304): onPanStart
          I/flutter (16304): onPanUpdate
          I/flutter (16304): onPanUpdate
          I/flutter (16304): onPanEnd
        • 2.5.onScaleXXX

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

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

          I/flutter (16304): onScaleStart
          I/flutter (16304): onScaleUpdate
          I/flutter (16304): onScaleUpdate
          I/flutter (16304): onScaleUpdate
          I/flutter (16304): onScaleUpdate
          I/flutter (16304): onScaleUpdate
          I/flutter (16304): onScaleUpdate
          I/flutter (16304): onScaleUpdate
          I/flutter (16304): 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,
             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 ( 3039): down1
          I/flutter ( 3039): down0
          如果behavior值改为HitTestBehavior.opaque,则只会打印"down1"。
    • 忽略PointerEvent

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

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

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

       





     

  • 相关阅读:
    js格式化货币金额
    Mac idea maven 创建web项目
    The string "--" is not permitted within comments.
    Java从数据库读取页面树形菜单
    oracle 查询重复数据并且删除, 只保留一条数据重复数据
    SSM框架---使用MyBatis Generator自动创建代码
    前端 NPM常用命令行
    前端常用命令行
    Angular 常用命令行
    JS/JQuery 文档加载完成
  • 原文地址:https://www.cnblogs.com/lxlx1798/p/11097012.html
Copyright © 2011-2022 走看看