最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Flutter | 事件处理

    正文概述 掘金(345丶)   2021-04-01   711

    概述

    在移动端,各个平台或者 UI 系统的事件模型都是基本一致,即:一次完整的事件分为三个阶段,手指按下,移动,抬起,而其他的双击,拖动等都是基于这些事件的

    当指针按下时,Flutter 会对应用程序执行命中测试(Hit Test) ,以确定指针与屏幕接触的位置存在哪些 Widget,指针按下事件(以及该指针的后续事件)会被分发到由命中测试发现的最内部的组件,然后从哪里开始,事件会在组件树中向上冒泡,这些事件会从最内部的组件分发的组件树的根路径上的所有组件,这个 Web 开发浏览器的事件冒泡机制相似,但是 Flutter 中没有机制取消或者停止冒泡过程,而浏览器是可以停止的。

    原始指针事件处理

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

    Listener({
      Key key,
      this.onPointerDown, //手指按下回调
      this.onPointerMove, //手指移动回调
      this.onPointerUp,//手指抬起回调
      this.onPointerCancel,//触摸事件取消回调
      this.behavior = HitTestBehavior.deferToChild, //在命中测试期间如何表现
      Widget child
    })
    
    • behavior 在后面专门介绍

    示例:

    class EventTest extends StatefulWidget {
      @override
      _EventTestState createState() => _EventTestState();
    }
    
    class _EventTestState extends State<EventTest> {
      PointerEvent _event;
    
      @override
      Widget build(BuildContext context) {
        return Listener(
          child: Container(
            margin: EdgeInsets.only(top: 50),
            color: Colors.blue,
            alignment: Alignment.center,
            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}),
        );
      }
    }
    

    效果如下:

    Flutter | 事件处理

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

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

    上面只是一些常用属性,除了这些还有很多其他属性,可自行查看 API

    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") 。

      如果想让整个 300x150 的区域都能点击,我们可以将 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, //放开此行注释后可以"点透"
          )
        ],
      )
      

      上栗中,当注释掉最后一行代码,在左上角200x100 范围内非文本区域点击时(顶部组件透明区域),控制台只会打印 down0,也就是说顶部没有接收到事件,只有底部接收到了

      当放开注释后,再点击时顶部和底部都会接收到事件

    忽略 PinterEvent

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

    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 子树上,所以不会响应指针事件,

    但是 AbsorbPoniter 本身是可以接受指针事件的,所以会输出 up,如果将 AbsorbPointer 换成 IgnorePointer,那么两个都不会输出;

    手势识别

    GestuerDetector

    GestureDetector 是一个用于手势识别的功能性组件,我们可以通过它来识别各种手势

    GestureDetector 实际上是指针事件的语义化封装,下面我们来看一下各种手势识别。

    点击,双击,长按

    我们通过 GestureDetector 对 Container 进行手势识别,触发相应事件后,在 Container 上显示事件名,如下:

    class _EventTestState extends State<EventTest> {
      //事件名称
      String _operation = "";
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: GestureDetector(
            child: Container(
              width: 200,
              color: Colors.blue,
              alignment: Alignment.center,
              height: 100,
              child: Text(_operation, style: TextStyle(color: Colors.white,fontSize: 20)),
            ),
            onTap: () => upDateText("tap"), //单击
            onDoubleTap: () => upDateText("doubleTap"), //双击
            onLongPress: () => upDateText("longPress"), //长按
          ),
        );
      }
    
      void upDateText(String text) {
        setState(() {
          _operation = text;
        });
      }
    }
    

    Flutter | 事件处理

    拖动,滑动

    一次完整的手势过程是指用户手指按下到抬起的整个过程,期间,用户按下后可能会移动,也可能不移动。

    GestureDetector 对拖动和滑动事件时没有区分的,他们本质是一样的。

    GestureDetector 会把要监听的组件的原点(左上角)作为本次手势的原点,当监听组件上手指按下时,手势识别就会开始。例:

    class _EventTestState extends State<EventTest> with SingleTickerProviderStateMixin {
    
      double _top = 100.0; //距离顶部的偏移
      double _left = 100.0; //距离左边的偏移
      @override
      Widget build(BuildContext context) {
    
        return Scaffold(
          body: Stack(
            children: <Widget>[
              Positioned(
                top: _top,
                left: _left,
                child: GestureDetector(
                  child: CircleAvatar(child: Text("A")),
                  //手指按下回调
                  onPanDown: (DragDownDetails e) {
                    print('用户手指按下 ${e.globalPosition}');
                  },
                  //手指滑动回调
                  onPanUpdate: (DragUpdateDetails e) {
                    //滑动时,更新偏移
                    print('滑动');
                    setState(() {
                      _left += e.delta.dx;
                      _top += e.delta.dy;
                    });
                  },
                  onPanEnd: (DragEndDetails e) {
                    //滑动结束,打印 x,y轴速度
                    print(e.velocity);
                  },
                ),
              )
            ],
          ),
        );
      }
    }
    
    • globalPosition:此属性为用户按下时相对于屏幕(非父组件)原点的偏移
    • delta:当用户在屏幕上滑动时,会触发多次 Update 事件,dalta 指一次 Update 事件滑动的偏移量
    • velocity:该属性代表用户抬起时的滑动速度(包含x,y两个轴的),上例中没有处理抬起的速度,常见的效果是根据抬起手指的速度做一个减速动画

    效果如下:

    Flutter | 事件处理
    I/flutter ( 8239): 用户手指按下 Offset(134.9, 280.7)
    I/flutter ( 8239): 滑动
    I/chatty  ( 8239): uid=10152(com.flutter.flutter_study) 1.ui identical 302 lines
    I/flutter ( 8239): 滑动
    I/flutter ( 8239): Velocity(-59.6, 244.0)
    
    单一方向拖动

    在很多场景中,我们只需要沿着一个方向来拖动,如一个垂直方向的列表

    GestureDetector 支持特定方向的手势事件,例如:

    Positioned(
      top: _top,
      child: GestureDetector(
        child: CircleAvatar(child: Text("A")),
        //手指按下回调
        onPanDown: (DragDownDetails e) {
          print('用户手指按下 ${e.globalPosition}');
        },
        onVerticalDragUpdate: (DragUpdateDetails e) {
          setState(() {
            _top += e.delta.dy;
          });
        },
        onPanEnd: (DragEndDetails e) {
          //滑动结束,打印 x,y轴速度
          print(e.velocity);
        },
      ),
    )
    

    修改滑动的那个例子如上即可

    缩放

    GestureDetector 可以监听缩放事件,如下:

    Center(
      child: GestureDetector(
        child: Image.asset("./images/avatar.jpg", width: _width),
        onScaleUpdate: (ScaleUpdateDetails details) {
          setState(() {
            //缩放倍数在 0.8 到 10 倍之间
            _width = 100 * details.scale.clamp(.8, 10.0);
          });
        },
      ),
    );
    
    Flutter | 事件处理

    上例比较简单,实际中我们可能还需要一些其他功能,如双击放大缩小,执行动画等,有兴趣的可以先尝试一下

    GestureRecognizer

    getstureDetector 内部是使用一个或者多个 GestureRecognizer 来识别各种手势的,而 GestureRecognizer 的作用就是通过 Listener 将原始指针转换为语义手势

    GestureRecognizer 是一个抽象类,一种手势对应一个子类,Flutter 实现了丰富的手势识别器,我们可以直接使用。

    例如:

    我们要给一段富文本 (RichText) ,的不同部分添加事件处理器,但是 TextSpan 并不是一个 widget,所以不能用 GestureDetector。但是 TextSpan 有一个 Recongizer 属性,他可以接收一个 GestureRecognizer。

    bool _toggle = false; //变色开关
    TapGestureRecognizer _recognizer = TapGestureRecognizer();
    
    Widget bothDirectionTest() {
      return Center(
        child: Text.rich(TextSpan(children: [
          TextSpan(text: "你好世界"),
          TextSpan(
              text: "点击变色",
              style: TextStyle(
                  fontSize: 30, color: _toggle ? Colors.red : Colors.yellow),
              recognizer: _recognizer
                ..onTap = () {
                  setState(() {
                    _toggle = !_toggle;
                  });
                }),
          TextSpan(text: "你好世界")
        ])),
      );
    }
    @override
    void dispose() {
        //用到GestureRecognizer的话一定要调用其dispose方法释放资源
        _recognizer.dispose();
        super.dispose();
    }
    
    

    注意:使用 GestureRecognizer 之后,一定要调用其 dispose 方法来释放资源(主要是取消内部的计时器),运行效果如下:

    Flutter | 事件处理

    手势竞争与冲突

    竞争

    如在上例中,同时监听水平方向和垂直方向的拖动事件,那么斜着滑动时那个方向会生效? 实际上取决于第一次移动时两个轴上的位移分量,那个轴的大,那么哪个轴就会在本次滑动事件中胜出

    实际上 Flutter 中引入了一个 Arenal 的概念,直译为 竞技场 的意思,每一个手势识别器(GestureRecognizer) 都是一个竞争者(GestureArenaMember),当发生滑动事件时,他们都要在 竞技场 去竞争本次事件的处理权,而最终只有一个竞争者会胜出。

    例如有一个 ListView,他的第一个子组件也是 ListView,如果滑动子 ListView,父 ListView 会动吗?答案肯定是不会动的,这时只有子 ListView 会动,这是因为子 LsitView 货到了滑动事件的处理权。

    示例

    var _top1 = 100.0;
    var _left1 = 100.0;
    
    Widget bothDirection() {
      return Stack(
        children: [
          Positioned(
            top: _top1,
            left: _left1,
            child: GestureDetector(
              child: CircleAvatar(child: Text("A")),
              onVerticalDragUpdate: (DragUpdateDetails details) {
                setState(() {
                  _top1 += details.delta.dy;
                });
              },
              onHorizontalDragUpdate: (DragUpdateDetails details) {
                setState(() {
                  _left1 += details.delta.dx;
                });
              },
            ),
          )
        ],
      );
    }
    

    运行之后,每次拖动只会沿着一个方向移动,而竞争者发生在手指按下后首次移动时

    上例中获胜的条件是,首次移动时的位置在水平和垂直方向上分量大的一个获胜

    手势冲突

    由于手势竞争最终只有一个胜出者,所以,当有多个手势识别器时,可能会产生冲突;

    例如有一个 Widget,可以左右拖动,现在我们也想检测它上面手指按下和抬起的事件,如下:

    var _left2 = 100.0;
    Widget flictTest() {
      return Stack(
        children: [
          Positioned(
            left: _left2,
            top: 100,
            child: GestureDetector(
              child: CircleAvatar(child: Text("A")),
              onHorizontalDragUpdate: (DragUpdateDetails details) {
                setState(() {
                  _left2 += details.delta.dx;
                });
              },
              onHorizontalDragEnd: (details) {
                print('onHorizontalDragEnd');
              },
              onTapDown: (details) {
                print('down');
              },
              onTapUp: (details) {
                print('up');
              },
            ),
          )
        ],
      );
    }
    

    拖动后,日志如下:

    0I/flutter ( 4315): down
    I/flutter ( 4315): onHorizontalDragEnd
    

    我们发现没有打印 up,这是因为拖动时,在按下手指没有移动时,拖动手势还没有完整的语义,此时 TapDown 手势胜出,此时打印 down,而拖动时,拖动手势胜出,当抬起时, onHorizontalDragEnd 和 onTap 发生冲突,但是应为是在拖动的语义中,所以 onHorizeontalDragend 胜出,所以就会打印 onHorizontalDragEnd。

    如果我们的逻辑代码中,对手指的按下和抬起时强依赖的,例如轮播组件,我们希望按下时暂停轮播,抬起时恢复轮播。但是由于轮播组件中本身可能已经处理了拖动手势,甚至支持了缩放手势,这时外部如果再用 onTapDown,onTap 来监听是不行的。

    这个时候就可以同个 Listener 监听原始指针事件就行:

    Listener(
        child: GestureDetector(
          child: CircleAvatar(child: Text("A")),
          onHorizontalDragUpdate: (DragUpdateDetails details) {
            setState(() {
              _left2 += details.delta.dx;
            });
          },
          onHorizontalDragEnd: (details) {
            print('onHorizontalDragEnd');
          },
        ),
        onPointerDown: (details){
          print('onPointerDown');
        },
        onPointerUp: (details){
          print('onPointerUp');
        },
      ),
    )
    

    手势冲突只是手势级别的,而手势是对原始指针的语义化识别,所以在遇到复杂的冲突场景时,都可以通过 Listener 直接识别原始指针事件来解决冲突

    事件总线

    在 App 中,我们经常需要一个广播机制,用以夸页面事件通知,例如注销登录时,某些页面可能需要进行状态更新。这个时候一个事件总线便会非常有用;

    事件总线通常实现了订阅者模式,订阅者包含订阅者和发布者两个角色,可以通过事件总线来触发事件和监听事件;

    代码如下:

    typedef void EventCallback(arg);
    
    class EventBus {
      //私有构造
      EventBus._internal();
    
      static EventBus _singleton = new EventBus._internal();
    
      //工厂构造函数
      factory EventBus() => _singleton;
    
      //保存时间订阅者队列,key:事件名(id),value:对应的实际订阅者队列
      var _eMap = new Map<Object, List<EventCallback>>();
    
      ///添加订阅者
      void on(eventName, EventCallback f) {
        if (eventName == null || f == null) return;
        _eMap[eventName] ??= [];
        _eMap[eventName].add(f);
      }
    
      ///移除订阅者
      void off(eventName, [EventCallback f]) {
        var list = _eMap[eventName];
        if (eventName == null || list == null) return;
        if (f == null) {
          _eMap[eventName] = null;
        } else {
          list.remove(f);
        }
      }
    	
      ///触发订阅者
      void emit(eventName, [arg]) {
        var list = _eMap[eventName];
        if (list == null) return;
        int len = list.length - 1;
        for (var i = len; i > -1; i--) {
          list[i](arg);
        }
      }
    }
    
    ///定义一个 top-level,全局变量,页面引入该文件之后可以直接使用 bug
    var bus = new EventBus();
    

    使用如下:

    //监听登录失效
    bus.on(Event.LOGIN_OUT, (arg) {
      SpUtil.putString(Application.accessToken, null);
      Application.router.navigateTo(context, Routes.login, clearStack: true);
    });
    
    //触发失效事件
    bus.emit(Event.LOGIN_OUT, null);
    

    事件总线常用于组件之间的状态共享,但是关于组件之间的状态共享也有一些专门的包,如 redux,以及 Provider。

    对于一些简单的应用,事件总线总是奏议满足业务需求,如果觉得使用状态管理包的话,一定要想清楚 APP 是否有必要使用它,防止化简为繁的过度设计。


    参考


    起源地下载网 » Flutter | 事件处理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元