Flutter - 详情页 TabBar 与模块联动?秒了!

欢迎关注微信公众号:FSA全栈行动 👋

一、前言

如图,这是个非常常见的功能,像淘宝、唯品会、朴朴等 App 的商品详情页都可以看到。

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

顺带提一嘴,朴朴(版本: V5.6.8-0-1c4424)的这个功能有个小 bug,在点击 Tab 的时候可以将对应模块滚动到 TabBar 下方,但是它在滑动时却是从列表的顶部开始计算的,所以在点击了第二个 Tab 后,列表往下稍微滑一点点,就会切回第一个 Tab 了,是故意的还是不小心?还是故意不小心呢?🤔

OK,接下来我们就来看看如何通过我的开源库快速实现该功能

github.com/fluttercand...

二、结构

页面结构很简单,主体为 ListViewDetailNavBar 内部是使用 TabBar 来实现的,将其盖在 ListView 的上方。

dart 复制代码
return Stack(
  children: [
    const DetailListView(),
    const Positioned(
      top: 0,
      left: 0,
      right: 0,
      child: DetailNavBar(),
    ),
  ],
);

三、功能实现

我们先来完成滚动过程中更新 DetailNavBarindex 的功能

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: 用于设置计算可视区域的偏移量,如图中的绿线,一般情况下,判断 ListViewitem 是否为第一个,主要是看它是否接触绿线,当 dynamicLeadingOffset 返回的值越大,绿线就越向下偏移,计算的可视区域变小,第一个 item 也随之变化。

所以这里的 dynamicLeadingOffset 返回了 DetailNavBar 的高度 state.navBarHeight,就是从 DetailNavBar 的下方开始计算可视区域,计算第一个 item

2、scrollNotificationPredicate: 用于是否处理滚动通知,ListViewObserver 内部是监听滚动通知来触发观察的,但在一些场景下,如 item 内部有轮播,轮播在每次翻页时也会发出滚动通知,不过其 depth 大于 0,所以可以用来判断当前的滚动通知是否为被观察的 ListView 发出的, defaultScrollNotificationPredicateFlutter 自带的方法,代码如下:

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、处理观察结果

ListViewObserveronObserve 回调方法中我们可以拿到观察结果,通过观察结果我们就可以计算出当前 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;
}

传值说明

  • ListView8 个模块,对应的下标就是 0 ~ 7
  • DetailNavBar 显示的是 模块 1、4、7Tab,对应模块的下标就是 [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 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~

相关推荐
加班是不可能的,除非双倍日工资2 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi2 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip2 小时前
vite和webpack打包结构控制
前端·javascript
excel3 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国3 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼3 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy3 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT3 小时前
promise & async await总结
前端
Jerry说前后端3 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天4 小时前
A12预装app
linux·服务器·前端