前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何响应用户交互事件

如何响应用户交互事件

作者头像
拉维
发布2019-08-22 16:55:29
2.1K0
发布2019-08-22 16:55:29
举报
文章被收录于专栏:iOS小生活iOS小生活iOS小生活

今天我们来聊聊Flutter是如何监听和响应用户的手势操作的。

手势操作在Flutter中分为两类:

  • 第一类是原始的指针事件(Pointer Event),即原生开发中常见的触摸事件,表示屏幕上的触摸(或鼠标、手写笔)行为触发的位移行为。
  • 第二类则是手势识别(Gesture Detector),表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装。

接下来,我们先来看一下原始的指针事件。

指针事件

指针事件表示用户交互的原始触摸数据,如手指接触屏幕 PointerDownEvent、手指在屏幕上移动 PointerMoveEvent、手指抬起 PointerUpEvent,以及触摸取消 PointerCancleEvent,这与原生系统的底层触摸事件抽象是一致的。

在手指接触屏幕,接触事件发起时,Flutter会确定手指与屏幕发生接触的位置上究竟有哪些组件,并将触摸事件交给最内层的组件去响应。事件会从这个最内层的组件开始,沿着组件树向根节点向上分发。

Flutter无法取消或停止事件的进一步分发,我们只能通过hitTestBehavior去调整组件在命中测试期内应该如何表现,比如把触摸事件交给子组件或者交给其视图层级之下的组件去响应。

关于组件层面的原始指针事件的监听,Flutter提供了Listener Widget,可以监听其子Widget的原始指针事件。

现在,我们一起来看一个Listener的案例。我们定义一个宽为300的红色正方形Container,利用Container监听其内部Down、Move及Up事件:

Listener(
  child: Container(
    color: Colors.red,// 背景色红色
    width: 300,
    height: 300,
  ),
  onPointerDown: (event) => print("down $event"),// 手势按下回调
  onPointerMove:  (event) => print("move $event"),// 手势移动回调
  onPointerUp:  (event) => print("up $event"),// 手势抬起回调
);

我们试着在红色正方形区域内进行触摸点击、移动、抬起,可以看到 Listener 监听到了一系列原始指针事件,并打印出了这些事件的位置信息:

I/flutter (13829): up PointerUpEvent(Offset(97.7, 287.7))
I/flutter (13829): down PointerDownEvent(Offset(150.8, 313.4))
I/flutter (13829): move PointerMoveEvent(Offset(152.0, 313.4))
I/flutter (13829): move PointerMoveEvent(Offset(154.6, 313.4))
I/flutter (13829): up PointerUpEvent(Offset(157.1, 312.3))

手势识别

使用Listener可以直接监听指针事件。不过指针事件毕竟太原始了,如果我们想要获取更多的触摸事件细节,比如判断用户是否正在拖拽控件,直接使用指针事件的话就会非常复杂。

通常情况下,响应用户交互行为的话,我们会使用封装了手势语义操作的Gesture,如点击 onTap、双击 onDoubleTap、长按 onLongPress、拖拽 onPanUpdate、缩放 onScaleUpdate 等。另外,Gesture 可以支持同时分发多个手势交互行为,意味着我们可以通过Gesture同时监听多个事件

Gesture是手势语义的抽象,而如果我们想从组件层监听手势,则需要使用GestureDetector。GestureDetector 是一个处理各种高级用户触摸行为的Widget,与Listener一样,也是一个功能性组件

接下来我们通过一个案例来看看GestureDetector的用法。

我定义了一个Stack层叠布局,使用Positioned组件将一个红色的Container放置在左上角,并同时监听点击、双击、长按和拖拽事件。在拖拽事件的回调方法中,我们更新了Container的位置:

// 红色 container 坐标
double _top = 0.0;
double _left = 0.0;
Stack(// 使用 Stack 组件去叠加视图,便于直接控制视图坐标
  children: <Widget>[
    Positioned(
      top: _top,
      left: _left,
      child: GestureDetector(// 手势识别
        child: Container(color: Colors.red,width: 50,height: 50),// 红色子视图
        onTap: ()=>print("Tap"),// 点击回调
        onDoubleTap: ()=>print("Double Tap"),// 双击回调
        onLongPress: ()=>print("Long Press"),// 长按回调
        onPanUpdate: (e) {// 拖动回调
          setState(() {
            // 更新位置
            _left += e.delta.dx;
            _top += e.delta.dy;
          });
        },
      ),
    )
  ],
);

运行这段代码,并查看控制台输出,可以看到,红色的Container除了可以响应我们的拖拽行为之外,还能够同时响应点击、双击、长按这些事件。

尽管在上面的例子中,我们对一个Widget同时监听多个手势事件,但最终只会有一个手势能够得到本次事件的处理权。对于多个手势的识别,Flutter引入了手势竞技场(Arena)的概念,用来识别究竟哪个手势可以响应用户事件。手势竞技场会考虑用户触摸屏幕的时长、位移以及拖动方向,来确定最终手势。

那手势竞技场具体是怎样实现的呢?

实际上,GestureDetector 内部对每一个手势都建立了一个工厂类(Gesture Factory)。而工厂类的内部会使用手势识别类(Gesture Recognizer),来确定当前处理的手势。

而所有手势的工厂类都会被交给RawGestureDetector 类,以完成监测手势的大量工作:使用Listener监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器,然后这些手势会在竞技场决定最后由谁来响应用户事件。手势识别器会根据用户交互的位置、加速度、方向等因子综合判断当前需要以哪个手势去响应,这是确定的不确定的是如果你的交互具有二义性,而你需要识别的多个手势之间又非常相似(比如旋转和缩放),则最后到底哪个手势去响应需要综合PK一下。

有些时候我们可能会在应用中给多个视图注册同类型的手势监听器,比如微博的信息流列表中的微博,点击不同区域会有不同的响应:点击头像会进入用户个人主页,点击图片会进入查看大图页面,点击其他部分会进入微博详情页等。

像这样的手势识别发生在多个存在父子关系的视图时,手势竞技场会一并检查父视图和子视图的手势,并且通常最终会确认由子视图来响应事件。而这也是合乎常理的:从视觉效果上看,子视图的视图层级位于父视图之上,相当于对其进行了遮挡,因此从事件处理上看,子视图自然是事件响应的第一责任人。

从下面的实例中,我定义了两个嵌套的Container容器,分别加入了点击识别事件:

GestureDetector(
  onTap: () => print('Parent tapped'),// 父视图的点击回调
  child: Container(
    color: Colors.pinkAccent,
    child: Center(
      child: GestureDetector(
        onTap: () => print('Child tapped'),// 子视图的点击回调
        child: Container(
          color: Colors.blueAccent,
          width: 200.0,
          height: 200.0,
        ),
      ),
    ),
  ),
);

运行这段代码,然后在蓝色区域进行点击,可以发现:尽管父容器也监听了点击事件,但Flutter只响应了子容器的点击事件。

I/flutter (16188): Child tapped

为了让父容器也能接收到手势,我们需要同时使用 RowGestureDetector 和 GestureFactory,来改变竞技场决定由谁来响应用户事件的结果。

在此之前,我们还需要自定义一个手势识别器,让这个识别器在竞技场被PK失败时,能够再把自己重新添加回来,以便接下来还能继续去响应用户事件。

在下面的代码中,我定义了一个继承自点击手势识别器 TapGestureRecognizer的类,并重写了其rejectGesture方法,手动地把自己又复活了

class MultipleTapGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    acceptGesture(pointer);
  }
}

接下来,我们需要将手势识别器和其工厂类传递给RawGestureDetector,以便用户产生手势交互事件时能够立刻找到对应的识别方法。事实上,RawGestureDetector的初始化函数所做的配置工作,就是定义不同手势识别器和其工厂类的映射关系

这里,由于我们只需要春处理点击事件,所以只配置一个识别器即可。工厂类的初始化采用 GestureRecognizerFactoryWithHandlers函数完成,这个函数提供了手势识别对象创建,以及对应的初始化入口。

在下面的代码中,我们完成了自定义手势识别器的创建,并设置了点击事件回调方法。需要注意的是,由于我们只需要在父容器监听子容器的点击事件,所以只需要将父容器用 RowGestureDetector包装起来就可以了,而子容器保持不变:

RawGestureDetector(// 自己构造父 Widget 的手势识别映射关系
  gestures: {
    // 建立多手势识别器与手势识别工厂类的映射关系,从而返回可以响应该手势的 recognizer
    MultipleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
        MultipleTapGestureRecognizer>(
          () => MultipleTapGestureRecognizer(),
          (MultipleTapGestureRecognizer instance) {
        instance.onTap = () => print('parent tapped ');// 点击回调
      },
    )
  },
  child: Container(
    color: Colors.pinkAccent,
    child: Center(
      child: GestureDetector(// 子视图可以继续使用 GestureDetector
        onTap: () => print('Child tapped'),
        child: Container(...),
      ),
    ),
  ),
);

运行一下这段代码,我们可以看到,当点击蓝色容器时,其父容器也收到了Tap事件:

I/flutter (16188): Child tapped
I/flutter (16188): parent tapped 

总结

现在我们来简单回顾下Flutter是如何来响应用户事件的。

首先,我们了解了Flutter底层原始指针事件,以及对应的监听方式和冒泡分发机制。

然后我们学习了封装了底层指针事件手势语义的Gesture,了解了多个手势的识别方法,以及其同时支持多个手势交互的能力。

最后,我们介绍了Gesture的事件处理机制:在Flutter中,尽管我们可以对一个Widget监听多个手势,或者对多个Widget监听同一个手势,但Flutter会使用手势竞技场来进行各个手势的PK,以保证最终只会有一个手势能够响应用户行为如果我们希望同时能有多个手势去响应用户行为,那就需要去自定义手势,利用RawGestureDetector和手势工厂类,在竞技场PK失败时,手动把它复活

在处理多个手势识别场景时,很容易出现手势冲突的问题。比如,当需要对图片进行点击、长按、旋转、缩放、拖动等操作的时候,如何识别用户当前是点击还是长按,是旋转还是缩放。如果想要精确地处理复杂交互手势,我们势必需要介入手势识别过程,解决异常

不过需要注意的是,冲突的只是手势的语义化识别过程,原始指针事件是不会冲突的。所以在遇到复杂的冲突场景通过手势很难搞定时,我们也可以通过Listener 直接识别原始指针事件,从而解决手势识别的冲突

以上。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档