Flutter - 子部件任意位置观察滚动数据
# 一、概述
在 scrollview_observer (opens new window) 的 1.23.0 中,新增了允许对观察结果进行监听的功能,也就是说你可以不需要在固定的观察结果回调(如:onObserve、onObserveAll )中去处理数据了。
那具体是什么意思呢?我们通过实战来了解一下。
# 二、实战
这里以之前的 Flutter - 轻松搞定炫酷视差(Parallax)效果 为例

# Page
这里附上原先 PageView 的构建代码,以及使用 ListViewObserver 对其进行观察,并在 onObserve 回调中实现 Parallax 效果的详细处理逻辑
final observerController = ListObserverController();
Widget _buildPageView() {
Widget resultWidget = PageView.builder(
...
);
resultWidget = ListViewObserver(
controller: observerController,
child: resultWidget,
triggerOnObserveType: ObserverTriggerOnObserveType.directly,
onObserve: (resultModel) {
final displayingChildModelList = resultModel.displayingChildModelList;
for (var itemModel in displayingChildModelList) {
// 取出 item 的下标
final itemIndex = itemModel.index;
// 取出 item 自身的显示占比
final itemDisplayPercentage = itemModel.displayPercentage;
// 计算无符号的 alignment.x
double itemAlignmentX = 1 - itemDisplayPercentage;
// 计算有符号的 alignment.x
if (itemModel.leadingMarginToViewport > 0) {
itemAlignmentX = -itemAlignmentX;
}
// 取值范围判断
if (itemAlignmentX > 1) {
itemAlignmentX = 1;
} else if (itemAlignmentX < -1) {
itemAlignmentX = -1;
}
// 赋值
pageItemBgPicAlignmentXList[itemIndex].value = itemAlignmentX;
}
},
customTargetRenderSliverType: (renderObj) {
return renderObj is RenderSliverFillViewport;
},
);
...
return resultWidget;
}
在上代码中,我们在 onObserve 回调中去处理了 Parallax 效果的一切逻辑,但假设 item 有多种类型呢?比如可能会在其中插一个不需要 Parallax 效果的纯广告图片的 item,这个时候就得去取出对应下标的数据,判断是否需要该效果,否则就 return 不做处理。
那此时就会想,我们能不能仅在需要 Parallax 效果的 item 内部去做呢?
可以,我们先将 onObserve 回调去除,保留 ListViewObserver 及其配置,即保留观察能力,为 item 提供数据。
final observerController = ListObserverController();
Widget _buildPageView() {
Widget resultWidget = PageView.builder(
...
);
resultWidget = ListViewObserver(
controller: observerController,
child: resultWidget,
triggerOnObserveType: ObserverTriggerOnObserveType.directly,
customTargetRenderSliverType: (renderObj) {
return renderObj is RenderSliverFillViewport;
},
);
...
return resultWidget;
}
# Item
将 item 抽成 StatefulWidget,并传入下标和对应的数据
class ParallaxItemView extends StatefulWidget {
final int index;
final String imgUrl;
const ParallaxItemView({
Key? key,
required this.index,
required this.imgUrl,
}) : super(key: key);
State<ParallaxItemView> createState() => _ParallaxItemViewState();
}
附上视图布局代码,主要靠 picAlignmentX 来控制背景的对齐偏移,进而实现视差效果。
class _ParallaxItemViewState extends State<ParallaxItemView> {
final picAlignmentX = ValueNotifier<double>(0);
...
void dispose() {
...
picAlignmentX.dispose();
super.dispose();
}
Widget build(BuildContext context) {
Widget resultWidget = Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: _buildPageItemBgPicView(widget.index),
),
...
],
);
...
return resultWidget;
}
Widget _buildPageItemBgPicView(int index) {
return ValueListenableBuilder(
valueListenable: picAlignmentX,
builder: (BuildContext context, double alignmentX, Widget? child) {
return Image.network(
widget.imgUrl,
fit: BoxFit.cover,
alignment: Alignment(alignmentX, 0),
);
},
);
}
}
好了,现在来实现在 item 中对观察结果数据的监听。
ListViewObserverState? observerState;
void didChangeDependencies() {
super.didChangeDependencies();
removeListener();
// 通过当前 item 的 context 找到 ListViewObserver 的 State
// 再调用 addListener 方法对其进行监听
observerState = ListViewObserver.of(context)
..addListener(
onObserve: handleObserverResult,
);
}
void dispose() {
...
removeListener();
super.dispose();
}
/// 移除监听
void removeListener() {
observerState?.removeListener(
onObserve: handleObserverResult,
);
observerState = null;
}
/// 处理监听结果
void handleObserverResult(
ListViewObserveModel result,
) {
if (result.displayingChildModelMap.isEmpty) return;
// 根据 index 取出对应的观察结果数据
final model = result.displayingChildModelMap[widget.index];
// 取不到说明当前不在显示区内,重置 picAlignmentX
if (model == null) {
picAlignmentX.value = 0;
return;
}
// 计算无符号的 alignment.x
picAlignmentX.value = 1 - model.displayPercentage;
// 计算有符号的 alignment.x
if (model.leadingMarginToViewport > 0) {
picAlignmentX.value = -picAlignmentX.value;
}
// 取值范围判断
if (picAlignmentX.value > 1) {
picAlignmentX.value = 1;
} else if (picAlignmentX.value < -1) {
picAlignmentX.value = -1;
}
}
其实很简单,也就三步走
- 调用
addListener对观察结果进行监听 - 处理观察结果数据
- 在
dispose方法中调用removeListener移除监听
handleObserverResult 方法中关于 picAlignmentX 的计算在之前的 Flutter - 轻松搞定炫酷视差(Parallax)效果 一文中有详细讲解,这里就不再赘述。
# 三、其它说明
# of
根据 context 向上找最近的 ObserverWidgetState,如果找不到会抛异常。
observerState = ListViewObserver.of(context);
observerState = GridViewObserver.of(context);
observerState = SliverViewObserver.of(context);
# maybeOf
of 的可选类型版本,如果向上找不到对应类型的 ObserverWidgetState 则返回 null
observerState = ListViewObserver.maybeOf(context);
observerState = GridViewObserver.maybeOf(context);
observerState = SliverViewObserver.maybeOf(context);
# 嵌套问题
ObserverWidget 是可以嵌套使用的,如下代码所示
widget = getListView(
scrollController: scrollController,
itemCount: 100,
itemBuilder: (context, index) {
...
},
);
widget = ListViewObserver(
tag: tag2,
child: widget,
controller: observerController2,
);
widget = ListViewObserver(
tag: tag1,
child: widget,
controller: observerController1,
);
一个 ListView 被两个 ListViewObserver 所包裹。
如果此时我们直接使用 item 的 BuildContext 去调用 of,则会拿到离它最近的 tag2 的 ListViewObserverState。
// tag2 的 ListViewObserverState
ListViewObserver.of(itemContext)
那如果我们想要拿到的是 tag1 的 ListViewObserverState,该怎么做呢?
很简单,使用 tag 参数即可~
ListViewObserver.of(
context,
tag: tag1,
);
当然,你不使用 tag 参数,而是通过 tag2 的 ListViewObserver 对应的 BuildContext 去调用 of 也是可以的~
ListViewObserver.of(tag2Context);
# 四、最后
通过上述示例的讲解,相信你对 scrollview_observer 的使用又更加清楚,开源不易,如果你也觉得这个库好用,请不吝给个 Star 👍 ,并多多支持!
GitHub: https://github.com/fluttercandies/flutter_scrollview_observer (opens new window)
本篇到此结束,感谢大家的支持,我们下次再见! 👋

- 01
- Flutter 多仓库本地 Monorepo 方案与体验优化10-25
- 02
- Flutter webview 崩溃率上升怎么办?我的分析与解决方案10-19
- 03
- Flutter - Melos Pub workspaces 实践10-12