跨平台框架都会面对和原生平台沟通的问题,Flutter 也不例外,在理论工程落地的过程中常常会碰到手势辨认交互的问题。本文介绍了西瓜视频解决 Flutter 和 iOS 手势抵触的计划,具体内容如下。
茫茫人海中,你看到这一段文字,节约你三秒钟的工夫,欢送你来一场 iOS 交换技术的碰撞,互相学习,共同提高技术!563513413
Flutter 进阶:解决 iOS 手势抵触
背景
客户端日常开发中,手势辨认是交互设计中不可或缺的性能,为此 Flutter 和 iOS 都提供了一套手势零碎,同时,为了让 Flutter 页面融入进 iOS 原生 UI 中,Flutter 提供了一个 UIView 的子类(这里简称 FlutterView),所有的屏幕点击信息都会通过 UIView 定义的几个办法(touchBegin/Move/Cancel/End)传入 FlutterView,从而被 Flutter 手势零碎解决。
问题
西瓜视频在理论应用过程中发现了一个问题,场景是这样:西瓜 iOS 客户端所有页面都有全屏右划退出性能,这个性能的实现是将一个 PanGestureRecognizer 增加到 NavigationController 的 View 上,只有辨认到右划手势,就退出以后页面。
在测试的时候咱们发现 Flutter 页面的列表都不能划动了,怎么回事?
理解 iOS 手势的同学应该晓得一个常识:解决屏幕触摸事件时,GestureRecognizer 领有比 touchXXX 办法更高的优先级,默认状况下 GestureRecognizer 解决不了的触摸事件才会流转到 touchXXX 办法解决。
问题就是因为这个机制引起的:NavigationController 上的 PanGestureRecognizer 生产了所有的触摸事件,并没有把这些事件流转到 FlutterView,所以 Flutter 页面的所有手势都生效了。
第一次尝试
既然起因是 FlutterView 没有解决触摸事件的机会,那咱们尝试的指标也明确了:让 FlutterView 有解决的机会就好了,这个也很容易实现,iOS GestureRecognizer 有一个属性 cancelsTouchesInView,这个属性会管制 GestureRecognizer 要不要将触摸事件流转给 UIView 的 touchXXX 办法解决。
When this property is YES (the default) and the receiver recognizes its gesture, the touches of that gesture that are pending are not delivered to the view and previously delivered touches are cancelled through a touchesCancelled:withEvent: message sent to the view. If a gesture recognizer doesn’t recognize its gesture or if the value of this property is NO, the view receives all touches in the multi-touch sequence.
看上去咱们只需将 NavigationController 的 PanGestureRecognizer 的 cancelsTouchesInView 设置为 NO 即可。
批改完之后,理论测试发现还是有问题,尽管垂直滚动的列表能够失常滑动了,然而横向滚动的列表的体现是不对的:当有横划列表时,不仅列表在滚动,整个页面也在向右滑动做退出动画。
咱们冀望的交互成果是:当用户在划动横向列表时,全屏手势后退成果应该是不失效的才对。
问题的根本原因是全屏右划后退手势和 FlutterView 都在解决右划触摸事件,而绝大多数交互场景,咱们都应该遵循这样的准则:父控件和子控件都能解决某个手势时,应该优先让子控件解决,而不是父子都解决。
持续尝试
通过上次尝试,咱们发现单单让 FlutterView 失去解决触摸事件的机会是不够的,咱们还须要让 FlutterView 取得和 iOS GestureRecognizer『平等竞争』的机会,因为有很多场景咱们须要 FlutterView 单独解决触摸事件。
对于 iOS 的 UI 世界来说,FlutterView 是一个试图融入这个世界的『外人』,『外人』想在一个新环境『平等竞争』只有一条平安的路:相熟并利用新环境的『游戏规则』。那么什么是 iOS 的『游戏规则』呢?
这里针对手势场景列几条规定:
- GestureRecognizer 比 UIView 的 touchXXX 办法有更高的优先级。
- 所有的 GestureRecognizer 都能够平等竞争触摸事件的处理权。
- GestureRecognizer 能够依赖公开的机制(requireGestureRecognizerToFail,GestureRecognizerDelegate 等)精细化配置优先级和具体的行为。
FlutterView 就是一个 UIView 而已,因为规定 1 的限度,他天生低人一等,无奈平等竞争,而毁坏规定不事实,投机取巧又不短暂,如果你就是这个 FlutterView 该怎么办呢?
答案是:找个代理人,替你竞争。
代理人是什么?
代理人就是一个自定义的 GestureRecognizer(后续称为 ProxyGestureRecognizer),他次要负责以下事件:
- 接管 iOS 触摸事件,并传递给 FlutterView(cancelsTouchesInView 设置为 NO 即可)。
- 将 FlutterView 外部手势解决的状态映射成 GestureRecognizer 定义的状态。
- 依据状态去和其余 iOS GestureRecognizer 竞争后续触摸事件的处理权。
主体逻辑和一般的 GestureRecognizer 一样,只有第二项比拟非凡,一般的 GestureRecognizer 会依据本人外部逻辑来计算状态,而 ProxyGestureRecognizer 是依据 FlutterView 的手势解决状况来计算状态。
这里大部分状态转移的逻辑和实现一个一般 GestureRecognizer 很类似,只有 possible -> began 以及 possible -> failed 比拟非凡,这两个转移的意思是:如果 FlutterView 外部没有任何手势可能解决 possible 状态时传入的触摸事件,则状态变为 failed,即 FlutterView 放弃对后续触摸事件的处理权,反之,则状态变为 began,即 FlutterView 能够解决后续的触摸事件。
如果可能实现这样一个 ProxyGestureRecognizer,咱们就能够通过 requireGestureRecognizerToFail 办法让 ProxyGestureRecognizer 优先解决触摸事件,ProxyGestureRecognizer 解决不了再交给全屏后退手势。更进一步的,为了更好的用户体验,咱们能够通过 GestureRecognizerDelegate 设置屏幕最左侧 30 像素仍然优先交给全屏后退手势,这样能防止全屏都是横划列表的状况下无奈用手势后退的问题。所有这些精细化的配置都得益于后面说的根本办法:
相熟并利用 iOS 世界的『游戏规则』。
要害的状态转移代码如下:
复制代码
- (void)flutterHandleTouch:(BOOL)isWorking{if (isWorking && self.state == UIGestureRecognizerStatePossible) {self.state = UIGestureRecognizerStateBegan; return;} if (!isWorking && self.state == UIGestureRecognizerStatePossible) {self.state = UIGestureRecognizerStateFailed; return;}}
复制代码
取得 FlutterView 外部手势状态
上一节说了 ProxyGestureRecognizer 的状态转移比拟非凡,它须要晓得 FlutterView 外部有没有手势能解决触摸事件,以及何时开始解决。如果拿不到这些信息,就无奈实现这个 ProxyGestureRecognizer,那咱们能不能晓得这些信息呢?
论断是咱们能够通过自定义 Flutter GestureRecognizer 来取得这些信息。
(接下来进入 Flutter 的手势世界,因为 Flutter 手势名字也叫 GestureRecognizer,所以不要和 iOS 搞混哦~)
Flutter 的手势零碎有一个『手势竞技场』的概念,它负责解决手势抵触,手势抵触的胜者会被调用 acceptGesture,败者会被调用 rejectGesture。有了这个机制在,咱们只须要把一个自定义的 GestureRecognizer『送进』每一次手势抵触的竞技场,如果 acceptGesture 被调用了,则阐明没有任何其余 GestureRecognizer 可能解决触摸事件,反之如果 rejectGesture 被调用了,则阐明至多有一个其余 GestureRecognizer 可能解决触摸事件。
实现这样的自定义手势须要满足两个条件:
- 要能继续接管触摸事件,因为有些手势判断本人是否能解决须要破费肯定工夫(比方长按手势),如果自定义手势很快的就确定了本人能或不能接管触摸事件,则可能疏忽了长按类的手势。
- 要能与所有手势发生冲突。
通过测试发现 PanGestureRecognizer 就能满足第一个条件,咱们的自定义 GestureRecognizer 继承 PanGestureRecognizer 就能够了。
第二个条件也很容易达成:将自定义 GestureRecognizer 增加到根 Widget 外层,这样它就可能与所有的手势发生冲突。
取得了 FlutterView 外部手势是否在解决触摸事件的信息后,通过 Platform Channel 传递给 iOS 层的 ProxyGestureRecognizer,再由它实现上述的状态转移逻辑即可。
自定义手势代码如下:
复制代码
class _PointerTracker extends PanGestureRecognizer {bool _flutterGestureIsWorking = false; @override void rejectGesture(int pointer) {super.rejectGesture(pointer); _flutterGestureIsWorking = true; _notify();} @override void acceptGesture(int pointer) {super.acceptGesture(pointer); _flutterGestureIsWorking = false; _notify();} void _notify() { GestureConflict.flutterGestureStateChanged(_flutterGestureIsWorking); }}
复制代码
实现这个 GestureRecognizer 之后,只须要将他简略封装成一个 FlutterGestureTracker Widget,套在 Flutter 根 Widget 上即可工作。
要害代码如下:
复制代码
class FlutterGestureTracker extends StatelessWidget {FlutterGestureTracker({Key key,this.child}) : super(key: key); final Widget child; @override Widget build(BuildContext context) {return RawGestureDetector( behavior: HitTestBehavior.translucent, gestures: { _PointerTracker: GestureRecognizerFactoryWithHandlers< _PointerTracker>( () => _PointerTracker(), //constructor (_PointerTracker instance) {//initializer}, ) }, child: child, ); }}
复制代码
持续摸索
咱们应用了代理机制来解决这个问题,看上去曾经没事儿了,然而咱们的解决方案在实质上是将 Flutter 的外部状态映射成 iOS 的状态,因为两边的设计理念不统一,所以必然有些状况是难以一一映射的,比方 Flutter 里不止有 GestureRecognizer 可能解决触摸事件,Listener 也能够,因为 Listener 不会进入手势竞技场竞争,咱们的计划实际上是疏忽了 Listener 的。
目前有个思路是依赖 Dart Dill Transform 做 AOP,给 Listener 的回调办法注入一些逻辑来记录 Listener 是否在工作。这个办法咱们也在调研中,还不成熟,并且大部分状况下咱们都不举荐间接通过 Listener 监听触摸事件,官网也举荐应用 GestureDetector :
/// Rather than listening for raw pointer events, consider listening for
/// higher-level gestures using [GestureDetector].
如果你的我的项目肯定要依赖 Listener,心愿你审慎思考本文的计划,如果有其余兼容 Listener 的思路也欢送大家一起探讨。
总结
跨平台框架都会面对和原生平台沟通的问题,这是跨平台的实质决定的,Flutter 也不例外,咱们在理论工程落地的过程中踩的坑少数都是这类问题,实质上手势抵触的问题也属于这一类,后续碰到相似问题,大家能够尝试应用代理机制来解决。