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 - 危!3.24版本苹果审核被拒!11-13
- 02
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 03
- Flutter - 轻松实现PageView卡片偏移效果09-08