记一次Flutter手势响应问题排查
问题描述
近日QA提出一个问题:待办列表页面,左滑显示删除按钮手势 会导致其他手势操作卡顿,并且附上了录屏,从录屏中可以看出,页面在响应上下滑动、滑动删除等场景时都不是很灵敏,有时会没有正确响应。
简单描述一下该页面的功能吧:该页面主要用来展示某一种类型的列表,主体是一个ListView
,
列表条目采用flutter_slidable
包裹,以实现滑动删除条目功能,点击条目后,会跳转到待办详情页。页面主体被gesture_x_detector
包裹,可以侧滑关闭该页面。可以看出该页面的手势种类还是比较多的,且存在同方向的手势事件需要处理。
排查过程
最开始我尝试将外部侧滑手势控件移除,hot reload后发现手势响应问题依然存在,所以可以排除该控件的问题。之后决定从GestureArenaManager
中来开始探查,有多少手势参与了竞争,又是谁最终赢得了手势的处理权。
GestureArenaManager
的一些重要方法里有一些debug方法,用来输出一些日志信息:
追踪进去可以发现如果debugPrintGestureArenaDiagnostics
为true则会打印一些日志信息,这个参数是一个顶级变量,我们可以调试页面将其设置为true,这样GestureArenaManager
就会输出一些日志信息,方便调试。
打开日志后,尝试进行一些拖拽操作,此时确实感觉到页面对这些动作响应的不是很顺畅,此时查看日志输出:
发现无响应的时候,手势被一个PanGestureRecognizer
给消费了,这个手势识别器的判定比较宽泛,横向纵向的滑动都可以抢夺事件。那么这个PanGestureRecognizer
由何而来呢?此时需要借助 Flutter Inspector
工具协助分析一下Widget Tree。
由Android Studio右侧点开后,点击一下刷新按钮,以展示最新的Widget Tree。
Widget Tree展开后可以进行搜索,这里搜索GestureDetector
,我们需要关心的是关键页面下监听了pan手势的GestureDetector
,不过页面层级比较深的话这个功能可能不是很好用。好在Flutter Inspector
可以帮我们快速定位到我们当前所处的页面,当我们维护一些不是由自己开发的页面的时候,可以借助与Flutter Inspector
。
最终定位到一处可疑的地方,这个控件消费了onPanDown事件,但是没有做任何处理:
会不会就是这里导致页面响应出了问题呢?注释掉这一块,发现列表的滑动删除和上下滑动都可以十分顺畅的响应了,手势竞技场也不再有PanGestureRecognizer
抢夺事件了,到这里,问题似乎就已经解决了。
新的问题
可是还没完,此处代码注释后,会发现页面的侧滑关闭功能在某些条目上失效了。尝试一番操作后,查看日志,发现不同条目下赢得手势的识别器是不一样的:
追踪到上层处理侧滑手势的控件,发现是通过XGestureDetector
这一开源组件来识别手势的,这一控件基于Listener
实现,不参与手势竞争,可以直接监听一些触摸事件。在这里发现一处可疑代码,似乎是更新时候判断是否是垂直方向滚动,如果是,则不处理侧滑手势:
注释这段代码,发现原本不可滑动的条目现在可以滑动了,但是又有新的问题,页面在侧滑的同时,垂直列表也可以上下滑动,这样的体验会比较奇怪,需要换个思路。
为什么有些条目可以响应,有些不可以呢?查看待办条目控件的源码,可以发现支持侧滑的控件外层包了Slidable
控件,这样的控件是可以正常侧滑的,无法侧滑的控件外层没有这一控件,无法正常侧滑。
调整代码,在不可侧滑的控件外围也包裹Slidable
控件,但是不添加endActionPane
,发现原本不可侧滑的条目现在可以正常侧滑了,也没有引出新的问题,该问题至此算是解决了。
为什么包裹一层Slidable
后可以正常侧滑了?因为Slidable
内部有横向手势监听,这样在我们侧滑过程中可以抢夺外部列表的纵向滑动处理,最上层的侧滑控件内部判断其不是纵向滑动,才会继续处理侧滑关闭动作。
问题扩展
为什么要引入XGestureDetector
来处理侧滑呢?直接使用系统的GestureDetector
不可以吗?
这是因为Flutter自动的GestureDetector
会参与手势竞争,待办的条目控件的Slidable
在较为上层,两者都会添加HorizontalDragGestureRecognizer
去手势竞技场(GestureArenaManager
)竞争。在GestureBinding
分发手势事件的时候,WidgetTree较深的手势识别器会优先响应,响应后其他识别器会自动拒绝,因而无法处理。
不过这里我觉得官方的设计不是很好,判断滑动事件是否接受的逻辑在DragGestureRecognizer
的handleEvent
方法下内部处理了,经过一系列的判断后,最后会调用一下_hasSufficientGlobalDistanceToAccept
方法,如果该方法返回true,则判断该手势被消耗。
这个方法是私有的,没有对外暴露。上述问题中待办条目的横向手势识别器其实只处理了从右向左的滑动删除动作,从左向右的手势其实并不关心,也不应该消耗。如果该私有判断可以对外暴露,让开发者决定是否要处理,一些滑动冲突问题处理起来就会简单不少。
转载自:https://juejin.cn/post/7231081140789968953