首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

深入理解Flutter手势系统

导语 |Flutter作为一个跨平台开发框架,手势识别被放在Dart层。事件的收集和传递依赖各平台的不同实现,并将屏蔽事件对象的差异,统一转换为Flutter可识别的事件对象。Flutter采用竞技场模式对手势进行识别并决出最终获胜者。本文将从源码角度对Flutter事件传递及手势识别系统进行分析,希望与大家一同交流(本文论述基于Flutter 1.23.0)。文章作者:omegaxiao,腾讯PCG研发工程师。

一、事件传递

手势是对事件的语义化封装,手势的识别依赖于设备屏幕上所产生的各类事件(down/move/up/cancel)。无论在Android平台,还是iOS平台,事件均由平台原生父控件进行收集,在屏蔽平台差异后向Engine侧转发,事件最终将被发送至Framework层的GestureBinding中进行处理。在Android平台,事件传递的路径为java=>c++=>dart,具体流程如下图所示:

在Android平台事件最初由FlutterView(extends FrameLayout)接收, 随后对事件MotionEvent对象进行处理,提取其中关键数据(包括事件类型、坐标、时间戳等数据)序列化成ByteBuffer。

在平台层处理完事件数据后,通过JNI接口将事件的bytebuffer发送至Engine层:

在Engine层经过几道转发后,最终会在window中通过tonic将数据发送至dart层。

最终事件通过dart层的window对象传递到Gesturebinding中进行后续处理。

二、事件分发

前面说到事件数据经过层层转发,最终会到达dart层的GestureBinding中进行处理,在介绍下面事件分发流程之前,先了解下与之相关比较重要的几个类:

GestureBinding: Flutter中手势识别的管理类,主要负责处理事件的分发;其中有几个重要的成员:pointerRouter、gestureArena、_hitTests;和几个比较重要的方法hitTest()和dispatchEvent();

RenderBinding: Flutter中渲染流程的管理类,在其中持有rootview对象(类似于Android中的根布局),是整颗渲染树的根,通过它可以遍历当前每一个渲染的对象。

HitTestTarget:接口,其中定义了一个handleEvent()方法,用于响应事件。在Flutter中,RenderObject继承自该类,因此所有RenderObject均可以作为事件的响应者。

Renderbox: 继承自RenderObject,其中定义了三个事件相关的方法:hitTest()、hitTestSelf()、hitTestChildren();其中hitTestSelf()和hitTestChildren()为抽象方法,需要子类去实现。hitTestSelf()的返回值true/false标识着该Renderbox是/否响应事件,hitTestChildren()方法用以控制该RenderBox孩子节点是否作为事件的响应者。

具体的处理流程如下(PointerDownEvent):

不同于Android的事件冒泡传递以及iOS的响应链机制,Flutter通过hitTest一次性获取该事件相关的所有组件,再逐一分发。在Flutter中,实际事件的响应者是这些组件所对应的RenderObject,并且通常为RenderBox对象。例如GestureDector所对应的RenderObject为RenderPointerListener。

1. hitTest

GestureBinding的hitTest()会首先调用到RenderBinding中的hitTest()方法,该方法从根节点(renderView)开始,根据子节点(RenderBox)hitTest规则向下遍历,将满足条件的子节点加入HitTestResult中,HitTestResult中持有一个hitTestTarget列表,存有当前事件对应的所有对象。我们看到,在遍历完所有子节点后,GestureBinding的hitTest将自身也加入了hitTestResult对象中。

在RenderBox的hitTest中,首先判断当前事件的位置是否落在自身的布局范围内,若在则继续通过对孩子节点进行hitTest,同时会通过hitTestSelf方法决定是否将自身加入响应者列表。RenderBox的子类可以通过重写hitTest()、hitTestChild()、hitTestSelf()方法自定义规则。

2. dispatchEvent

在hitTest获取当前事件所有响应者后,通过dispatchEvent分发事件。

不同于Android的事件传递,在Flutter中事件在传递过程中无法被拦截,hitTestResult中的每个响应者都会收到事件。对于大多数的RenderObject来说,其handleEvent()都是空实现。因为大多数组件可以通过增加一层来Listener实现对事件的响应。

另外从源码可以看出,在前面提到GestureDetector所对应的RenderObject为RenderPointerListener,这里我们直接看RenderPointerListner的handleEvent实现。

我们知道,每一种手势对应一个GestureRecognizer,在GestureDetector中注册了相应的回调方法,就会创建对应的GestureRecognizer。对于GestureDectector来说,只需要在PointerDownEvent事件发生时,将所有的GestureRecognizer添加到全局的路由中,之后的move/up之类的事件交由各个GestureRecognizer处理即可。因此这里只有onPointerDown方法被赋值,实际指向的是RawGestureDetectorState中的_handlePointerDown方法。

在_handlePointerDown中会遍历当前GestureDetector监听的所有GestureRecognizer,通过addPointer方法将这些GestureRecognizer添加到GestureBinding的pointerRouter中,以OneSequenceGestureRecognizer及其子类为例,会在startTrackingPointer中将当前手势识别器的handleEvent方法注册到路由,同时也会将当前识别器加入到竞技场,竞技场的相关逻辑会在后面专门讲述。需要注意的是,recognizers里GestureRecognizer是有固定顺序的,可以直接通过查看GestureDetector中创建顺序看到。

到这里,可以看到,每个GestureDetector对应的RenderPointerListner在被调用handleEvent后,会将监听的所有GestureRecognizer添加到GestureBindigng的pointerRouter中。这样一来,GestureBinding通过pointerRouter就能统一向所有的监听器转发事件。

前面说到,在hitTest遍历完所有子节点后,GestureBinding的hitTest将自身也加入了hitTestResult对象中。因此,在dispatchEvent的最后,会调用GestureBinding的handleEvent。

GestureBinding的handleEvent首先会向所有的GestureRecognizer转发Event事件,调用GestureRecognizer的handleEvent()方法处理事件。然后根据事件类型,执行竞技场的相关操作。

三、手势识别

在实际的开发过程中,我们可能需要对一些手势做出响应,例如:点击、双击、长按、滑动、缩放等。对于某个控件,有时候需要同时对多种手势进行不同的响应。例如某个按钮需要支持对点击和长按的监听,那么当用户操作发生时,如何决策哪一个手势应该得到响应,这里就需要引出我们的手势竞争机制。Flutter通过竞技场模型在众多手势中筛选出唯一的获胜者,响应该手势。

首先我们来看一下手势竞争机制的几个重要角色:

GestureRecognizer:手势识别器父类,相当于这场竞技中的选手,继承自GestureArenaMember。对于每一个选手,其具备主动接受(acceptGesture)或主动退出(rejectGesture)的两项权利。若识别器识别到当前事件流符合自身手势的逻辑,可主动申请接受(accept)此次事件。在当前的竞争机制下,最终只有一个竞争者会获得胜利。因此若竞争者主动接受则其他竞争者均需退出此次竞争;若竞争者主动退出,则该手势将从事件路由(pointerRouter)和竞技场(gestureArena)中移除,不再接受事件,剩余竞争者继续竞争。

__GestureArena:竞技场,竞争者参与竞技的场合,其中持有所有需要参与本次竞争的识别器(members),以及当前竞技场的状态:关闭(isOpen)、挂起(isHeld)、等待清扫(hasPendingSweep)。

GestureArenaManager:竞技场管理器,相当于这场竞技中的裁判,负责主持竞争的整个流程。裁判提供了几个在主持过程重要的能力:关闭(close)、挂起(hold)以及清扫(sweep)竞技场;关闭竞技场后,则不会有新的竞技者加入,竞争者可以开始竞争;若竞技场被挂起(hold),需等到持有者将其释放(release)后才能继续决出获胜者。

上一节说到GestureBinding在handleEvent中最后会根据事件类型,执行竞技场的相关操作。当PointerDownEvent发生时,会执行竞技场的关闭操作,标识着竞技开始,手势可以开始竞争。当PointerUpEvent发生时,会强制执行清扫操作,决出获胜者。我们先看GestureArenaManager的关闭操作。

在关闭竞技场后会尝试一次,查看当前状态下能否决出获胜者。如果当前只存在一个手势,那么这个手势将直接成为获胜者,其acceptGesture被调用,此次竞争结束,竞技场被移除。

手势的竞争实质上是参与竞争的各类手势,在接收到事件流后,根据自身的规则做出接受和拒绝的响应。这套规则就直接定义在GestureRecognizer的handleEvent方法中。

总结一下竞技场的几个规则:

任何时候,如果只剩下一个竞争者,则其直接作为获胜者,响应手势;

在接收事件的过程中,竞争者可以随时决定接受或者拒绝,第一个提出接受的手势获得响应权,其余均会被动退出竞争。这里需要注意的是,GestureRecognizer在事件路由中是按照深度越深排在越前的规则进行排列的。

竞争者如果选择退出竞争,则其不再接收事件。其余竞争者继续竞争直到以下三种情况任一种出现:

a. 有竞争者选择接受;

b. 只剩下一个竞争者;

c. up事件触发GestureAreanManager打扫竞技场;

接下来我们以两个场景为例来分析一下多手势下的竞争流程:

1. 场景一:同时监听tap手势和drag手势

先分别看下TapGestureRecognizer和DragGestureRecognizer的handleEvent()方法。TapGestureRecognizer的handleEvent()方法实现在父类PrimaryPointerGestureRecognizer中。

可以看出,在收到move事件后,如果移动的距离超过一定阈值,TapGestureRecognizer会选择拒绝,退出竞争,同时停止事件接收。否则继续调用handlePrimaryPointer()处理事件。

当cancel事件到来时,TapGestureRecognizer会直接选择拒绝,退出竞争。当up事件到来,调用_checkUp()检查当前手势状态是否为接受(accept),如果是接受,则会回调到在GestureDetector中注册的onTap方法。

接着,我们再来看看DragGestureRecognizer的handleEvent()方法。

一旦move事件移动的距离达到drag手势的阈值,DragGestureRecognizer便会选择接受,从而调用GestureDetector中注册的drag相关的事件回调。由于drag手势中move是一段连续的操作,因此在将当前手势置为接受状态后,再有新的move事件过来,直接触发drag相关的回调即可。

来看下完整的流程:

其中蓝色开头为move事件,红色开头为up事件。图中流程对应的是move事件偏移量超过tap手势允许的最小阈值(最小值kTouchSlop = 18.0)的情况,这个情况下,TapGestureRecognizer会通知到GestureAreanManager拒绝本次手势,不再参与竞争。

GestureAreanManager每一次在收到竞争者退出的通知后都会查看当前竞争者的数量,如果只剩下一个竞争者,则直接宣布其为获胜者。此处我们只设置了tap和drag手势,由于tap的退出,drag直接获得了胜利,随后drag的start和update被调用,最终该手势在GestureDetector注册的相关回调被调用。最后up事件到来,由于tap早早退出了竞争,不再接收事件。up事件就直接分发给了dragGestureRecognizer。

2. 场景二:同时监听tap手势和doubleTap手势

这里我们直接看DoubleTapGestureRecognizer的handleEvent()方法。

在PointerUpEvent事件到来的时候,会通过registerFirstTap()和registerSecondTap()方法分别标记两次点击事件。

在registerFirstTap()中看到,这里会启动一个定时器。Flutter中定义了构成双击事件的两次点击之间的最大时间间隔,默认值为300ms。紧接着立马调用竞技场的hold()方法,挂起竞技场,这意味着,在此之后300ms,竞技场都将处理挂起状态,即使PointerUpEvent到来,也不会触发竞技场的sweep操作。如果超过300md,下一次点击事件还未完成,则会将竞技场释放(release)。

这里我们就不画时序图,简单用文字梳理下tap手势和doubleTap手势的竞争过程:

第一次up事件先后分发给TapGestureRecognizer和DoubleTapGestureRecognizer,此时TapGestureRecognizer还未接受事件,DoubleTapGestureRecognizer记录第一次点击,启动定时器,并且将竞技场挂起。这之后,up事件虽然触发GestureArenaManager的sweep操作,但由于当前竞技场是挂起状态,sweep操作直接返回。

如果300ms内,发生了第二次点击事件,此时DoubleTapGestureRecognizer会记录第二次点击事件,通知GestureArenaManager接受手势,停止定时器并触发DoubleTap的回调。

如果300ms内,未发生第二次点击事件,则在定时器到时间后,会通过reset方法,通知GestureArenaManager拒绝手势并释放竞技场,之后会触发竞技场的sweep方法,TapGestureRecognizer获得竞争胜利。

四、结语

最后再总结一下,Flutter是通过hitTest找到所有事件响应者,并在down事件到来时,将监听的所有手势识别器GestureRecognizer加入事件路由以及竞技场中,并按顺序向他们转发事件。而GestureRecognizer接收事件后按自己对应的手势规则决定接受/拒绝响应,最终决出获胜的手势,回调GestureDetector中注册的相应方法。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20210201A0BJWN00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券