Flutter - 支持观察NestedScrollView,兼容性更强 😈
# 一、概述
时间过得真快,距离上一篇介绍 scrollview_observer
功能的文章已过去了 9个月
,目前 scrollview_observer
也来到了 1.21.0
版本,现在就带大家来看看都更新了哪些内容吧
GitHub: https://github.com/fluttercandies/flutter_scrollview_observer
# 二、功能点
# 支持 NestedScrollView
scrollview_observer
的 监听滚动视图中正在显示的子部件
与 滚动到指定下标位置
这两大功能,现已对 NestedScrollView
进行了支持,如下图所示
等下一篇文章再跟大家详细介绍使用步骤 : )
# 支持 center
如下代码所示,在一些场景下,你可能会对 CustomScrollView
的 center
进行设置,以此来实现聊天消息页。
Widget _buildScrollView() {
return CustomScrollView(
center: _centerKey,
anchor: 1,
controller: scrollController,
slivers: [
_buildSliverListView(
color: Colors.redAccent,
onBuild: (ctx) {
_sliverListCtx1 = ctx;
},
),
_buildSliverListView(
color: Colors.blueGrey,
onBuild: (ctx) {
_sliverListCtx2 = ctx;
},
),
// center
SliverPadding(padding: EdgeInsets.zero, key: _centerKey),
_buildSliverListView(
color: Colors.teal,
onBuild: (ctx) {
_sliverListCtx3 = ctx;
},
),
_buildSliverListView(
color: Colors.purple,
onBuild: (ctx) {
_sliverListCtx4 = ctx;
},
),
],
);
}
但又想使用下标跳转 item
的功能,放心,scrollview_observer
具有良好的兼容性,在 1.19.1
版本下就已得到支持,可随便你如何设置 center
,不需要额外做其它操作。
# observeIntervalForScrolling
相信大家都还记得,自动触发观察的时机有以下三种
枚举值 | 描述 |
---|---|
scrollStart | 开始滚动 |
scrollUpdate | 滚动中 |
scrollEnd | 结束滚动 |
其中 scrollUpdate
触发观察太过于频繁,其实很多次观察结果并不会相差多少,在大多数使用场景下,对我们来说也不太重要。
为此在本次更新中为 ObserverController
新增了 observeIntervalForScrolling
属性,用来设置触发观察的间隔,从而大量减少不必要的观察计算。
需要注意以下两点:
- 为了不改变之前的行为,所以默认值为
Duration.zero
,所以大家需要自行调整,推荐设置为Duration(milliseconds: 500)
。 - 该属性仅对
scrollUpdate
有效。
# visibleMainAxisSize
item
显示的尺寸大小
如图所示,各固定 200
高度的 item
在 ListView
中的 visibleMainAxisSize
。
# visibleFraction
在
sliver
中item
的显示占比计算公式:visibleFraction = visibleMainAxisSize / paintExtent
如图所示,当前 SliverList
的绘制长度 paintExtent
为 376
,其 item20
的可视大小 visibleMainAxisSize
为 30
,所以 item20
的可视占比 visibleFraction
为 30/376
。
# SliverObserveContext
用于获取 sliver
的 BuildContext
,这在观察 sliver
的场景下非常有用。
如下 CustomScrollView
配置了多个 sliver
Widget _buildScrollView() {
return CustomScrollView(
controller: scrollController,
physics: const ClampingScrollPhysics(),
slivers: [
// banner
SliverPersistentHeader(...),
// 中间任意视图
SliverObserveContextToBoxAdapter(...),
// tabBar
SliverPersistentHeader(...),
// 构建多个 SliverGird
...List.generate(modelList.length, (mainIndex) {
return _buildSectionGridView(mainIndex);
}),
],
);
}
我们需要观察当前哪个 SliverGrid
是第一个,然后去同步更新 TabBar
的选中下标。
/// 记录各 Sliver 的下标与 BuildContext
Map<int, BuildContext> sliverIndexCtxMap = {};
SliverViewObserver(
controller: sliverObserverController,
sliverContexts: () => sliverIndexCtxMap.values.toList(),
child: child,
onObserveViewport: (result) {
...
},
);
但在研发过程中,我们很有可能会给 SliverGrid
再加一层 Sliver
去添加装饰、间距等,而 onObserveViewport
只认最外层的 sliver
,所以在这里我们就用 SliverObserveContext
去进行包裹成为最外层,在其 onObserve
回调中就可以拿到对应的 BuildContext
并记录起来。
Widget _buildSectionGridView(int mainIndex) {
Widget resultWidget = SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(...),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// 拿到了 SliverGrid 的 BuildContext
return Container(
...
);
},
childCount: 10,
),
);
resultWidget = SliverPadding(
padding: const EdgeInsets.all(8),
sliver: resultWidget,
);
// 在最外层使用 SliverObserveContext 进行包裹,以获取是外层的 BuildContext。
resultWidget = SliverObserveContext(
onObserve: (context) {
sliverIndexCtxMap[mainIndex] = context;
},
child: resultWidget,
);
return resultWidget;
}
# 聊天保持位置功能
保持位置功能目前有三种模式可选
enum ChatScrollObserverHandleMode {
/// 常规模式
/// 来一条消息就插入一条消息
normal,
/// 生成式模式
/// 比如 ChatGPT 这种流式自更新的消息
generative,
/// 指定模式
/// 可以灵活指定用来做为参照的消息item
specified,
}
本次更新的是指定模式,因为原 refItemRelativeIndex
和 refItemRelativeIndexAfterUpdate
两个参数仅能表达相对下标之意,而无法表达参考坐标系,所以将其废弃。
新增 refItemIndex
与 refItemIndexAfterUpdate
,并结合 refIndexType
来更好地指定参考 item
。
我们先来看一下 refIndexType
的类型定义
enum ChatScrollObserverRefIndexType {
/// relativeIndex trailing
///
/// 6 | item16 | cacheExtent
/// ----------------- -----------------
/// 5 | item15 |
/// 4 | item14 |
/// 3 | item13 | 正在显示中的item
/// 2 | item12 |
/// 1 | item11 |
/// ----------------- -----------------
/// 0 | item10 | cacheExtent <---- start
///
/// leading
relativeIndexStartFromCacheExtent,
/// relativeIndex trailing
///
/// 5 | item16 | cacheExtent
/// ----------------- -----------------
/// 4 | item15 |
/// 3 | item14 |
/// 2 | item13 | 正在显示中的item
/// 1 | item12 |
/// 0 | item11 | <---- start
/// ----------------- -----------------
/// -1 | item10 | cacheExtent
///
/// leading
relativeIndexStartFromDisplaying,
/// 直接指定 item 的下标
itemIndex,
}
如上,一共有 3种
参考模式供你选择
relativeIndexStartFromCacheExtent
: 从渲染区的item
开始计算下标。这种模式一般用于普通消息插入,因为插入消息必定是在0
处,插入消息前后不变的就是原来的最新消息,其下标从0
变成了1
,此时refItemIndex
可指定为0
,而refItemIndexAfterUpdate
指定为1
。relativeIndexStartFromCacheExtent
: 从展示区的item
开始计算下标。该模式比较少用,一般是结合观察功能,因为通过观察功能,我们是可以轻松得知正在显示的item
有哪些,假设你此时对正在显示的第一个item
做了内容变更,但又不想影响第二个正在显示的item
的偏移,那这个模式正好适合当前这种的场景。因为改变前后不变的是第二个正在显示的item
,所以refItemIndex
指定为1
,refItemIndexAfterUpdate
也指定为1
。itemIndex
: 三种模式中最容易理解的模式,用来参照的item
的下标是什么,你就指定什么,比如上述中item11
发生了变化,我们想保持位置就可以拿不变的item12
来做参照,所以refItemIndex
和refItemIndexAfterUpdate
都指定为12
。
记住,不管你选择哪种参考模式,都需要注意的一点,即指定的参照 item
需要在变化前后都有被渲染,这样才能确保保持位置的功能可以正常生效!
# 三、最后
通过上述示例的讲解,相信你对 scrollview_observer
的使用又更加清楚,开源不易,如果你也觉得这个库好用,请不吝给个 Star
👍
GitHub: https://github.com/fluttercandies/flutter_scrollview_observer (opens new window)
本篇到此结束,感谢大家的支持,我们下次再见! 👋
![](https://cdn.jsdelivr.net/gh/FullStackAction/PicBed@resource/image/FSA_QR_bottom.png)
- 01
- Flutter - 升级到3.24后页面还会多次rebuild吗?🧐08-11
- 02
- Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻08-04
- 03
- Android - 云游戏本地悬浮输入框实现07-07