Flutter 中的 ScrollNotification 为啥收不到

文摘   科技   2024-03-28 09:31   山东  

2024年 第06篇


ScrollNotification 冒泡原理解析


  • ScrollNotification 冒泡原理

  • 问题分析与解决



01 需求



在做智家 APP 悬浮窗优化需求时,需要获取列表的滑动并通知悬浮窗进行收起或全部显示。

基础库同事已经把 基础逻辑整理好如下:

NotificationListener<ScrollNotification>(
onNotification: (notification){
//1.监听事件的类型
if (notification.depth == 0 && notification.metrics.axis == Axis.vertical) {
if (notification is ScrollStartNotification) {
print("开始滚动...");
} elseif (notification is ScrollUpdateNotification) {
//当前滚动的位置和总长度
if (!scrolling) {
scrolling = true;
UIMessage.fireEvent(ScrollPageMessage(scrolling));
}
} elseif (notification is ScrollEndNotification) {
print("滚动结束....");
if (scrolling) {
scrolling = false;
UIMessage.fireEvent(ScrollPageMessage(scrolling));
}
}
}
returnfalse;
},
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}),
);

逻辑很简单,用 NotificationListener 把我们的列表包装一下运行一下测试一下,没问题就把代码提交一下。不管懂不懂 Flutter 中的 ScrollNotification 的逻辑,工作简简单单,任务快速完成。

NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification){
/// 处理 notification 逻辑
returnfalse;
},
/// EasyReresh 为包含 Head 的方便下拉刷新的组件
child: EasyRefresh(
child: ListView()
)
)

运行起来效果很正常,能正常收到 ScrollNotification

但是需求要求下拉时不能收起悬浮窗。那很很简单,下拉是 EasyRefresh 的行为,那用 NotificationListener 包一下 EasyRefreshchild 就好了。代码改成下面的样子。

EasyRefresh(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification){
/// 处理 notification 逻辑
returnfalse;
},
child: ListView()
)
)

运行一下发现 onNotification 不回调,这是为什么?


02 ScrollNotification 冒泡原理



ScrollNotification 是 Flutter 中常见通知,用于通知页面滑动。

以下为从 ScrollNotification 的产生到获取来说明滑动通知的流程。

触发页面滑动的原因有两种,手动和程序控制。手动则涉及 GestureDetector ,程序控制则可以调用 ScrollControllerjumpTo(double value) 方法。

因为程序控制相对监听手势滑动更简单,所以从 ScrollController.jumpTo(double value) 入手,看一下里面有没有发送 ScrollNotification

/// scroll_controller.dart
void jumpTo(double value) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
for (final ScrollPosition position inList<ScrollPosition>.from(_positions))
position.jumpTo(value);
}

当调用 ScrollControllerjumpTo(double value) 时会继续调用 ScrollPositionjumpTo(double value)ScrollPosition 是抽象类,具体实现类有多种,以滑动常见的 Listview 为例会调用 scroll_position_with_single_context.dart  中 ScrollPositionWithSingleContext

/// scroll_position_with_single_context.dart
@override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
finaldouble oldPixels = pixels;
forcePixels(value);
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
}
goBallistic(0.0);
}

从上面代码中的可以看到这里调用了 开始滑动,滑动中,结束滑动,正好对应 ScrollNotification 的三个实现子类 ScrollStartNotification ScrollUpdateNotification ScrollEndNotification

继续跟踪

///scroll_position.dart
void didUpdateScrollPositionBy(double delta) {
activity!.dispatchScrollUpdateNotification(copyWith(), context.notificationContext!, delta);
}
/// scroll_activity.dart
void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta).dispatch(context);
}

跟踪到重点了,这里是分发通知逻辑

///notification_listener.dart

void dispatch(BuildContext? target) {
// The `target` may be null if the subtree the notification is supposed to be
// dispatched in is in the process of being disposed. target?.visitAncestorElements(visitAncestor);
target?.visitAncestorElements(visitAncestor);
}

...

@protected
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final StatelessWidget widget = element.widget;
/// 遇到 NotificationListener 组件则调用它的 _dispatch 方法,并根据返回值判断是否继续_
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
returnfalse;
}
}
returntrue;
}
/// framework.dart

void visitAncestorElements(bool visitor(Element element)) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element? ancestor = _parent;
/// 循环获得 父 Element 并传给 visitor 方法
while (ancestor != null && visitor(ancestor))
ancestor = ancestor._parent;
}
/// notification_listener.dart
bool _dispatch(Notification notification, Element element) {
if (onNotification != null && notification is T) {
finalbool result = onNotification!(notification);
return result == true; // so that null and false have the same effect
}
returnfalse;
}

把上面三段组合起来看,

  • visitAncestorElements 用来循环向上查找父 Element,停止条件是 父 Element 是 null 或者 visitor 方法返回了 false

  • visitAncestor 方法再遇到 NotificationListener 方法时会调用它的 _dispatch() 方法,之后又调用了 onNotification() 方法

  • onNotification 返回 true 则冒泡停止,事件不再向父传递,返回 false 则冒泡继续向上传。所以修改 onNotification() 的返回值可以拦截冒泡。

以上是冒泡的产生和传递,通过以上的代码可以想到 使用  NotificationListener 将组件包起来即可得到组件上传的通知。

NotificationListener<ScrollNotification>(  
onNotification: (ScrollNotification notification) {
/// deal notification
returnfalse;
},
child: ListView()
)

以上是 ScrollNotification 的产生,传递,接受的流程。


03 问题分析



假如有下面布局代码,当滑动页面时,下面的两个 onNotification 一定能收到回调么?

/// EasyRefresh 为自定义组件

NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
print('outer onNotification $notification');
returnfalse;
},
child: EasyRefresh(
child: NotificationListener<ScrollNotification>(
/// 这里的 onNotification 收到回调么?
onNotification: (ScrollNotification scrollNotification) {
print('inner onNotification $scrollNotification');
returnfalse;
},
child: CustomScrollView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
/// ListView
child: ListView.builder(
controller: _scrollController,
shrinkWrap: true,
itemCount: 100,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return Text('data $index');
}),
)
],
),
),
))

按照刚才的分析中只要ListView滑动,在 ListView 与 外层的 NotificationListener 中间没有其他的组件拦截,则 内外层的 NotificationListener 都应该会被回调 onNotification  方法。

然而在实际测试中,只有外层的 outer onNotificationxxxx 被打印出来,内层的 inner onNotificationxxx 没有打印。

按理说既然外部都收到 ScrollNotification 通知了,内部应该更能收到通知才对。但是查看 EasyRefresh 源码,把它解构出来,得到如下代码。这段代码也是只会打印 最外层的 outer onNotification xxx 。这是因为手势滑动时其实是最外层的 CustomScrollView 带着 ListView 滑动,CustomScrollView 发送了 ScrollNotification 而不是 ListView 。所以内部的 NotificationListener 没有回调 onNotification

NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
print('outer onNotification ${notification}');
returnfalse;
},
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
print('middle onNotification ${notification}');
returnfalse;
},
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollNotification) {
print('inner onNotification $scrollNotification');
returnfalse;
},
child: CustomScrollView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: ListView.builder(
controller: _scrollController,
shrinkWrap: true,
itemCount: 100,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return Text('data $index');
}),
)
],
),
),
))
],
))

如何让 ListView 可以滚动?给 ListView 一个固定高度,并且 physics 不是 NeverScrollableScrollPhysics()

Container(
height: 600,
child: NotificationListener<ScrollNotification>(
onNotification:
(ScrollNotification scrollNotification) {
print(
'inner onNotification $scrollNotification');
returnfalse;
},
child: ListView.builder(
shrinkWrap: true,
controller: _scrollController,
itemCount: 100,
// physics: NeverScrollableScrollPhysics(),
itemBuilder:
(BuildContext context, int index) {
return Text('data $index');
}),
),
)



04 解决问题


因为实际业务中列表较为复杂,修改列表层级需要再仔细分析代码逻辑容易引起问题。所以还是在 EasyRefresh 外层进行监听,并根据 scrollNotification.metrics.pixels 是否小于 0 来判断是否下拉刷新可以将影响范围降到最小。


05 总结


  • 通知冒泡原理为组件层层向上传递通知,直到根组件或者某 onNotification() 返回 true 拦截通知的组件

  • 没收到通知也可能是因为子组件没有滑动,没有发送通知,而不一定是中间有组件拦截。

  • ListView 不是一定会滑动


06 团队介绍


三翼鸟数字化技术平台-交易交付平台」负责搭建门店数字化转型工具,包括:海尔智家体验店小程序、三翼鸟工作台APP、商家中心等产品形态,通过数字化工具,实现门店的用户上平台、交互上平台、交易上平台、交付上平台,从而助力海尔专卖店的零售转型,并实现三翼鸟店的场景创新。


    _________________ END__________________

三翼鸟数字化科技
三翼鸟数字化技术团队官方订阅号,提供技术前沿洞察、技术实践分享、最佳实践整合、技术规范发布、团队文化输出。