Flutter - 详情页 TabBar 与模块联动?秒了!
# 一、前言
如图,这是个非常常见的功能,像淘宝、唯品会、朴朴等 App
的商品详情页都可以看到。
简陋的 Demo
可科学上网后在线体验:
https://fluttercandies.github.io/flutter_scrollview_observer/#/detail (opens new window)
顺带提一嘴,朴朴(版本: V5.6.8-0-1c4424)的这个功能有个小
bug
,在点击Tab
的时候可以将对应模块滚动到TabBar
下方,但是它在滑动时却是从列表的顶部开始计算的,所以在点击了第二个Tab
后,列表往下稍微滑一点点,就会切回第一个Tab
了,是故意的还是不小心?还是故意不小心呢?🤔
OK,接下来我们就来看看如何通过我的开源库快速实现该功能
https://github.com/fluttercandies/flutter_scrollview_observer (opens new window)
# 二、结构
页面结构很简单,主体为 ListView
,DetailNavBar
内部是使用 TabBar
来实现的,将其盖在 ListView
的上方。
return Stack(
children: [
const DetailListView(),
const Positioned(
top: 0,
left: 0,
right: 0,
child: DetailNavBar(),
),
],
);
# 三、功能实现
我们先来完成滚动过程中更新 DetailNavBar
的 index
的功能
# 1、配置观察
创建两个必要的控制器
ScrollController scrollController = ScrollController();
late ListObserverController observerController = ListObserverController(
controller: scrollController,
)
// 不缓存 item 的偏移量
//
// 由于我的 ListView 有动态加载的模块,导致模块的偏移量并非固定,
// 所以这里设置为 false
// 如果你的 ListView 的模块都是是固定的,则建议设置为 true,这样
// 下次可以直接取值跳转,避免计算
..cacheJumpIndexOffset = false;
使用 ListViewObserver
包裹 ListView
进行观察
// ListView (没什么需要改动的,怎么实现都可以)
Widget resultWidget = ListView.separated(
...
);
// 使用 ListViewObserver 包裹 ListView 进行观察
resultWidget = ListViewObserver(
// 观察控制器
controller: state.observerController,
// DetailNavBar 的高度
dynamicLeadingOffset: () => state.navBarHeight,
// 观察结果回调
onObserve: logic.onObserveForListView,
// 只监听处理第一层滚动通知,即 notification.depth == 0
scrollNotificationPredicate: defaultScrollNotificationPredicate,
child: resultWidget,
);
这里重点说两个参数
1、dynamicLeadingOffset
: 用于设置计算可视区域的偏移量,如图中的绿线,一般情况下,判断 ListView
的 item
是否为第一个,主要是看它是否接触绿线,当 dynamicLeadingOffset
返回的值越大,绿线就越向下偏移,计算的可视区域变小,第一个 item
也随之变化。
所以这里的 dynamicLeadingOffset
返回了 DetailNavBar
的高度 state.navBarHeight
,就是从 DetailNavBar
的下方开始计算可视区域,计算第一个 item
。
2、scrollNotificationPredicate
: 用于是否处理滚动通知,ListViewObserver
内部是监听滚动通知来触发观察的,但在一些场景下,如 item
内部有轮播,轮播在每次翻页时也会发出滚动通知,不过其 depth
大于 0
,所以可以用来判断当前的滚动通知是否为被观察的 ListView
发出的, defaultScrollNotificationPredicate
是 Flutter
自带的方法,代码如下:
/// A [ScrollNotificationPredicate] that checks whether
/// `notification.depth == 0`, which means that the notification
/// did not bubble through any intervening scrolling widgets.
bool defaultScrollNotificationPredicate(ScrollNotification notification) {
return notification.depth == 0;
}
为了不改变先前版本的行为,scrollview_observer
并没有将其设置做为默认值,需要手动配置~
# 2、处理观察结果
在 ListViewObserver
的 onObserve
回调方法中我们可以拿到观察结果,通过观察结果我们就可以计算出当前 DetailNavBar
应该设置的 index
。
这里 scrollview_observer
已经将计算逻辑封装至 ObserverUtils.calcAnchorTabIndex
方法中,按如下调用即可。
void onObserveForListView(ListViewObserveModel result) {
final navBarTabController = state.navBarTabController;
if (navBarTabController == null) return;
// 计算 TabBar 的下标
final index = ObserverUtils.calcAnchorTabIndex(
observeModel: result,
tabIndexes: state.navBarTabs.map((e) => e.index).toList(),
currentTabIndex: navBarTabController.index,
);
// 更新 TabBar 的下标
updateNavBarTabIndex(index);
}
void updateNavBarTabIndex(int index) {
final navBarTabController = state.navBarTabController;
if (navBarTabController == null) return;
navBarTabController.index = index;
}
传值说明
ListView
有8
个模块,对应的下标就是0 ~ 7
DetailNavBar
显示的是模块 1、4、7
的Tab
,对应模块的下标就是[0, 3, 6]
上述 calcAnchorTabIndex
方法中的 tabIndexes
,传入的就是 [0, 3, 6]
,然后计算出 DetailNavBar
对应的 [0, 1 ,2]
# 3、点击跳转
void handleNavBarTabTap(int index) {
if (!state.scrollController.hasClients) return;
final tabModel = state.navBarTabs[index];
final moduleIndex = tabModel.index;
// 第一个模块,直接回到顶部
if (moduleIndex == 0) {
state.scrollController.jumpTo(0);
return;
}
// 其它模块
state.observerController.jumpTo(
index: moduleIndex,
offset: (_) => state.navBarHeight,
);
}
点击跳转第一个模块时,我们是知道偏移量的,直接 scrollController.jumpTo(0)
即可,避免不必要的计算。
点击跳转其它模块时,使用 observerController.jumpTo
,其中 offset
用于设置向下偏移多少,这里返回了 DetailNavBar
的高度 state.navBarHeight
,即滚动到 DetailNavBar
的下方。
# 四、最后
通过上述示例的讲解,相信你对 scrollview_observer
的使用又更加清楚,开源不易,如果你也觉得这个库好用,请不吝给个 Star
👍 ,并多多支持!
GitHub: https://github.com/fluttercandies/flutter_scrollview_observer (opens new window)
本篇到此结束,感谢大家的支持,我们下次再见! 👋

- 01
- Flutter - 使用本地 DevTools 验证 SVG 加载优化08-07
- 02
- AI - Gemini CLI 摆脱终端限制07-27
- 03
- Flutter - 聊天面板库动画生硬?这次让你丝滑个够07-20