欢迎关注微信公众号:FSA全栈行动 👋
一、前言
如图,这是个非常常见的功能,像淘宝、唯品会、朴朴等 App 的商品详情页都可以看到。

简陋的 Demo 可科学上网后在线体验: fluttercandies.github.io/flutter_scr...

顺带提一嘴,朴朴(版本: V5.6.8-0-1c4424)的这个功能有个小
bug,在点击Tab的时候可以将对应模块滚动到TabBar下方,但是它在滑动时却是从列表的顶部开始计算的,所以在点击了第二个Tab后,列表往下稍微滑一点点,就会切回第一个Tab了,是故意的还是不小心?还是故意不小心呢?🤔
OK,接下来我们就来看看如何通过我的开源库快速实现该功能
二、结构
页面结构很简单,主体为 ListView,DetailNavBar 内部是使用 TabBar 来实现的,将其盖在 ListView 的上方。
dart
return Stack(
children: [
const DetailListView(),
const Positioned(
top: 0,
left: 0,
right: 0,
child: DetailNavBar(),
),
],
);
三、功能实现
我们先来完成滚动过程中更新 DetailNavBar 的 index 的功能
1、配置观察
创建两个必要的控制器
dart
ScrollController scrollController = ScrollController();
late ListObserverController observerController = ListObserverController(
controller: scrollController,
)
// 不缓存 item 的偏移量
//
// 由于我的 ListView 有动态加载的模块,导致模块的偏移量并非固定,
// 所以这里设置为 false
// 如果你的 ListView 的模块都是是固定的,则建议设置为 true,这样
// 下次可以直接取值跳转,避免计算
..cacheJumpIndexOffset = false;
使用 ListViewObserver 包裹 ListView 进行观察
dart
// 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 自带的方法,代码如下:
dart
/// 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 方法中,按如下调用即可。
dart
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 ~ 7DetailNavBar显示的是模块 1、4、7的Tab,对应模块的下标就是[0, 3, 6]
上述 calcAnchorTabIndex 方法中的 tabIndexes,传入的就是 [0, 3, 6],然后计算出 DetailNavBar 对应的 [0, 1 ,2]
3、点击跳转
dart
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: github.com/fluttercand...
本篇到此结束,感谢大家的支持,我们下次再见! 👋
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有
iOS技术,还有Android,Flutter,Python等文章, 可能有你想要了解的技能知识点哦~