Flutter | 事件处理(上)

简介: Flutter | 事件处理(上)

概述



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


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


注意:只有通过命中测试的组件才能触发事件


原始指针事件处理


Flutter 中可以使用 Listener 来监听原始触摸事件,按照 中的分类,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}),
    );
  }
}


效果如下:


0a2653c851af460fa595bd959398a8f1.png


手指在蓝色区域内移动即可看到当前指针偏移,当触发指针事件时,参数 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 ,则可以使用 IgnorePointer 和 AbsorbPointer,这两个组件都能阻止子树接受指针事件,不同之处在于 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;
    });
  }
}


注意:当同时监听 onTop 和 onDoubleTap 时,当用户触发 tap 事件时,会有 200 毫秒的延时,这是因为可能会再次点击触发双击事件


如果只监听了 onTap,则不会有延时


拖动,滑动


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


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两个轴的),上例中没有处理抬起的速度,常见的效果是根据抬起手指的速度做一个减速动画

效果如下:


2019082413041482.gif


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);
      });
    },
  ),
);


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


相关文章
|
6月前
|
传感器 Android开发 iOS开发
Flutter插件开发指南02: 事件订阅 EventChannel
上一节我们讲了 Channel 通道,但是如果你是卫星定位业务,原生端主动推消息给 Flutter 这时候就要用到 EventChannel 通道了。 本节会写一个 1~50 的计数器,到 50 后自动关闭原生的消息订阅。
148 1
Flutter插件开发指南02:  事件订阅 EventChannel
|
Android开发
Flutter(十三)——事件处理:手势识别与事件通知
Flutter(十三)——事件处理:手势识别与事件通知
344 2
Flutter(十三)——事件处理:手势识别与事件通知
|
Dart JavaScript
Flutter | 事件处理(下)
Flutter | 事件处理(下)
Flutter | 事件处理(下)
flutter事件evevt详解
在Flutter中,手势有两个不同的层次: 第一层:原始指针事件(Pointer Events):描述了屏幕上由触摸板、鼠标、指示笔等触发的指针的位置和移动。 第二层:手势识别(Gesture Detector):这个是在原始事件上的一种封装。
191 0
Flutter中焦点FocusNode使用分析Flutter输入框焦点事件的捕捉与监听
在Flutter使用FocusNode来捕捉监听TextField的焦点获取与失去,同时也可通过FocusNode来使用绑定对应的TextField获取焦点与失去焦点。
Flutter中焦点FocusNode使用分析Flutter输入框焦点事件的捕捉与监听
|
29天前
|
Android开发 iOS开发 容器
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
|
18天前
|
开发者
鸿蒙Flutter实战:07-混合开发
鸿蒙Flutter混合开发支持两种模式:1) 基于har包,便于主项目开发者无需关心Flutter细节,但不支持热重载;2) 基于源码依赖,利于代码维护与热重载,需配置Flutter环境。项目结构包括AppScope、flutter_module等目录,适用于不同开发需求。
53 3
|
3天前
|
传感器 开发框架 物联网
鸿蒙next选择 Flutter 开发跨平台应用的原因
鸿蒙(HarmonyOS)是华为推出的一款旨在实现多设备无缝连接的操作系统。为了实现这一目标,鸿蒙选择了 Flutter 作为主要的跨平台应用开发框架。Flutter 的跨平台能力、高性能、丰富的生态支持和与鸿蒙系统的良好兼容性,使其成为理想的选择。通过 Flutter,开发者可以高效地构建和部署多平台应用,推动鸿蒙生态的快速发展。
79 0
|
5天前
|
Dart 安全 UED
Flutter&鸿蒙next中的表单封装:提升开发效率与用户体验
在移动应用开发中,表单是用户与应用交互的重要界面。本文介绍了如何在Flutter中封装表单,以提升开发效率和用户体验。通过代码复用、集中管理和一致性的优势,封装表单组件可以简化开发流程。文章详细讲解了Flutter表单的基础、封装方法和表单验证技巧,帮助开发者构建健壮且用户友好的应用。
55 0