先看一个效果图,TabBarView中有横向滚动组件应该是很常见,但操作速度比较快的时候,很容易复现一个滑动会冲突的问题
问题详述
- 滑动第一个tab,不管有没有切换到第二个tab
- 在TabBarView的滚动动画还未结束的时候,或者视觉上TabBarView的滑动已经结束了,尝试滑动第一个tab里的横向滚动ListView或者竖直的ListView,会发现事件被TabBarVie拦截了,这点交互体验很不好,像是个bug
问题分析
既然是事件的冲突问题,不妨回忆下flutter的事件处理,可以参考我之前的文章
Flutter事件分发和冲突处理
Flutter事件分发-源码角度解析HitTestBehavior
按着之前的思路回忆了一遍,发现问题应该不是在down-move-up这里引起的,因为在up触发之后,手势竞技场已经清扫成员了,如果是下一个down事件进来,那会重新走事件竞争,内部的ListView做为孩子应该是优先胜利的,除非TabBarView把事件屏蔽了,所以。。翻源码看看
源码分析
flutter代码版本为1.22,从TabBarView开始,看下TabBarView实际的实现是什么
TabBarViewState的build方法
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: PageView(
dragStartBehavior: widget.dragStartBehavior,
controller: _pageController,
physics: widget.physics == null
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
: const PageScrollPhysics().applyTo(widget.physics),
children: _childrenWithKey,
),
);
}
会发现TabBarView的实现是PageViwe,直接跳到_PageViewState的build方法
// 删除了一些和本文不相关的代码
@override
Widget build(BuildContext context) {
// 移除不必要的代码
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
child: Scrollable(
, ViewportOffset position) {
return Viewport(
slivers: <Widget>[
],
);
},
),
);
}
会发现是 NotificationListener->Scrollable->Viewport
NotificationListener是滚动通知,Viewport是控制内容展示,手势拦截应该不会在这两个组件里,那接着看
Scrollable。
通过查看ScrollableState的build方法,发现有个IgnorePointer,IgnorePointer可以拦截child对事件的响应,应该就是这了。
控制拦截child事件的是_shouldIgnorePointer变量
IgnorePointer(
key: _ignorePointerKey,
ignoring: _shouldIgnorePointer,
ignoringSemantics: false,
child: widget.viewportBuilder(context, position),
),
接着看_shouldIgnorePointer变量的具体逻辑,引用这个变量的就一个方法,逻辑很简单,当外部调用setIgnorePointer方法时,通过globalKey找到IgnorePointer对应的RenderObject,也就是RenderIgnorePointer,并修改ignoring。(关于IgnorePointer的原理这里不解释了)
@override
@protected
void setIgnorePointer(bool value) {
if (_shouldIgnorePointer == value)
return;
_shouldIgnorePointer = value;
if (_ignorePointerKey.currentContext != null) {
final RenderIgnorePointer renderBox = _ignorePointerKey.currentContext.findRenderObject() as RenderIgnorePointer;
renderBox.ignoring = _shouldIgnorePointer;
}
}
通过查看setIgnorePointer的方法注释可以发现这个方法就是我们这个问题的引发者。
setIgnorePointer的调用时机
1. 开始滚动到时候,调用那setIgnorePointer的(true)
2. 滚动结束的时候,也就是动画执行结束的时候调用那setIgnorePointer(false)
实际解决
先看效果
一开始是想在手势up的时候调用那setIgnorePointer(false),但由于此时手势已经被拦xi截,拿不到up的调用时机
此次的解决是重写了系统的TabBarView ,在外部构造PageController,传递给TabBarView里的PageView,然后监听ScrollStartNotification事件,手动调用setIgnorePointer(false)
伪代码如下
NotificationListener<ScrollNotification>(
child: TestTabBarView(
children: children,
onCreatePageController: () {
return pageController;
},
),
onNotification: (nofi) {
if (nofi is ScrollStartNotification) {
pageController?.position?.context?.setIgnorePointer(false);
}
return false;
},
),
然后重写系统的TabBarView,因为暂时没找到入口去调用setIgnorePointer,现在是同个pageController的position去调用的。
这里解释下为什么要重写的TabBarView,因为系统的的TabBarView内部默认构造了PageView的pageController,外部拿不到这个变量,只能加个入参吧我们的pageController传递进去
// TestTabBarView
const TestTabBarView({
Key key,
@required this.children,
this.controller,
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
this.onCreatePageController,
})
final PageController Function() onCreatePageController;
//在_TestTabBarViewState里
@override
void didChangeDependencies() {
super.didChangeDependencies();
_pageController = widget.onCreatePageController() ??
PageController(initialPage: _currentIndex ?? 0);
}
最后
首先说这个问题,应该算是正常的交互,但用户操作比较快时,还是很容易复现的。
第二是目前的解决方式一点都不优雅,后续如果有更好的方式再更新