Flutter 中的 ScrollNotification 为啥收不到

1. 需求

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

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

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

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

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

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

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

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

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

2. 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 in List<ScrollPosition>.from(_positions))  position.jumpTo(value);  
}

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

/// scroll_position_with_single_context.dart
@override  
void jumpTo(double value) {  goIdle();  if (pixels != value) {  final double 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.dartvoid dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta).dispatch(context);}

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

///notification_listener.dartvoid 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  return false;  }  }  return true;  
}
/// framework.dartvoid visitAncestorElements(bool visitor(Element element)) {  assert(_debugCheckStateIsActiveForAncestorLookup());  Element? ancestor = _parent;  /// 循环获得 父 Element 并传给 visitor 方法while (ancestor != null && visitor(ancestor))  ancestor = ancestor._parent;  
}
/// notification_listener.dartbool _dispatch(Notification notification, Element element) {if (onNotification != null && notification is T) {final bool result = onNotification!(notification);return result == true; // so that null and false have the same effect}return false;}

把上面三段组合起来看,

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

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

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

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

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

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

3. 问题分析

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

/// EasyRefresh 为自定义组件NotificationListener<ScrollNotification>(onNotification: (ScrollNotification notification) {print('outer onNotification $notification');return false;},child: EasyRefresh(child: NotificationListener<ScrollNotification>(/// 这里的 onNotification 收到回调么?onNotification: (ScrollNotification scrollNotification) {print('inner onNotification $scrollNotification');return false;},child: CustomScrollView(shrinkWrap: true,physics: ClampingScrollPhysics(),slivers: <Widget>[SliverToBoxAdapter(/// ListViewchild: 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}');return false;},child: CustomScrollView(slivers: [SliverToBoxAdapter(child: NotificationListener<ScrollNotification>(onNotification: (ScrollNotification notification) {print('middle onNotification ${notification}');return false;},child: NotificationListener<ScrollNotification>(onNotification: (ScrollNotification scrollNotification) {print('inner onNotification $scrollNotification');return false;},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');return false;},child: ListView.builder(shrinkWrap: true,controller: _scrollController,itemCount: 100,// physics: NeverScrollableScrollPhysics(),itemBuilder:(BuildContext context, int index) {return Text('data $index');}),),
)

4. 解决问题

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

5. 总结

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

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

  • ListView 不是一定会滑动

6. 团队介绍

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2904787.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

ArcGIS Pro横向水平图例

终于知道ArcGIS Pro怎么调横向图例了&#xff01; 简单的像0一样 旋转&#xff0c;左转右转随便转 然后调整图例项间距就可以了&#xff0c;参数太多就随便试&#xff0c;总有一款适合你&#xff01; 要调整长度&#xff0c;就调整图例块的大小。完美&#xff01; 好不容易…

Mac系统中使用VSCode安装C#开发环境进行编译调试

VSCode安装插件 C#c# Dev Kit 安装Mac版本 .net .net下载地址 查看安装结果 dotnet --list-sdksdotnet --info配置环境变量 open -e ~/.bash_profile添加如下内容 export DOTNET_ROOT/usr/local/share/dotnet export PATH$PATH:$DOTNET_ROOT终端重新加载配置文件 sourc…

八大技术趋势案例(区块链量子计算)

科技巨变,未来已来,八大技术趋势引领数字化时代。信息技术的迅猛发展,深刻改变了我们的生活、工作和生产方式。人工智能、物联网、云计算、大数据、虚拟现实、增强现实、区块链、量子计算等新兴技术在各行各业得到广泛应用,为各个领域带来了新的活力和变革。 为了更好地了解…

AWS创建IAM用户,以及通过IAM用户登录

基本概念&#xff1a; IAM Identity Center&#xff08;AWS SSO&#xff09; 跨账户访问&#xff1a;IAM Identity Center允许用户使用他们自己的单一登录凭证来访问多个AWS账户和应用程序。这意味着你可以拥有一个账户和密码&#xff0c;通过IAM Identity Center的用户门户&…

LabVIEW单片机的废气再循环EGR检测系统

LabVIEW单片机的废气再循环EGR检测系统 实现了一种基于LabVIEW和STM32F103VET6单片机的EGR&#xff08;废气再循环&#xff09;检测系统&#xff0c;监测和控制船用二冲程柴油机的EGR运行状态。通过替代传统的NI采集卡&#xff0c;系统不仅降低了成本&#xff0c;同时也提升了数…

Capture One Pro 22 for Mac/win:重塑RAW图像处理的艺术

在数字摄影的世界里&#xff0c;RAW图像处理软件无疑是摄影师们手中的魔法棒&#xff0c;而Capture One Pro 22无疑是这一领域的璀璨明星。这款专为Mac和Windows系统打造的图像处理软件&#xff0c;以其出色的性能、丰富的功能和极致的用户体验&#xff0c;赢得了全球摄影师的广…

论文笔记:TALK LIKE A GRAPH: ENCODING GRAPHS FORLARGE LANGUAGE MODELS

ICLR 2024&#xff0c;reviewer评分 6666 1 intro 1.1 背景 当下LLM的限制 限制1&#xff1a;对非结构化文本的依赖 ——>模型有时会错过明显的逻辑推理或产生错误的结论限制2&#xff1a;LLMs本质上受到它们训练时间的限制&#xff0c;将“最新”信息纳入到不断变化的世…

寄主机显示器被快递搞坏了怎么办?怎么破?

大家好&#xff0c;我是平泽裕也。 最近&#xff0c;我在社区里看到很多关于开学后弟弟寄来的电脑显示器被快递损坏的帖子。 看到它真的让我感到难过。 如果有人的数码产品被快递损坏了&#xff0c;我会伤心很久。 那么今天就跟大家聊聊寄快递的一些小技巧。 作为一名曾经的…

为什么我的微信小程序 窗口背景色backgroundColor设置参数 无效的问题处理记录!

当我们在微信小程序 json 中设置 backgroundColor 时&#xff0c;实际在电脑的模拟器中根本看不到效果。 这是因为 backgroundColor 指的窗体背景颜色&#xff0c;而不是页面的背景颜色&#xff0c;即窗体下拉刷新或上拉加载时露出的背景。在电脑的模拟器中是看不到这个动作的…

计算机专业学习单片机有什么意义吗?

玩单片机跟玩计算机区别还是很大的, 单片机有众多的种类,每一种又可能有很多个系列.可以说单片机就是为了专款专用而生的.这样来达到产品成本的降低,这就是现在身边的很多的电子产品价格一降再降的原因之一.在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一…

北斗短报文+4G应急广播系统:实时监控 自动预警 保护校园安全的新力量

安全无小事&#xff0c;生命重如山。学生是祖国的未来&#xff0c;校园安全是全社会安全工作的一个重要的组成部分。它直接关系到青少年学生能否安健康地成长&#xff0c;关系到千千万万个家庭的幸福安宁和社会稳定。 灾害事故和突发事件频频发生&#xff0c;给学生、教职员工…

Docker大全

Docker大全 Docker安装准备工作开启虚拟机系统卸载Docker在线安装Docker离线安装Docker Docker服务基本操作启动docker服务查看docker状态设置docker开机自启禁用docker开机自启重新启动docker服务查看docker信息查看docker info中具体key的信息停止docker服务docker镜像加速 D…

ETLCloud结合Oracle实现CDC

CDC&#xff0c;即Change Data Capture&#xff08;变更数据捕获&#xff09;功能&#xff0c;主要针对实时数据同步和更新场景&#xff0c;能够实时监测数据库中的数据变化&#xff0c;并将发生变化的数据进行高效精准地捕获和传输&#xff0c;极大地提高了数据处理的效率以及…

Mybatis别名 动态sql语句 分页查询

给Mybatis的实体类起别名 给Mybatis的xml文件注册mapper映射文件 动态sql语句 1 if 2 choose 3 where 4 foreach 一&#xff09;if 查询指定名称商品信息 语法&#xff1a; SELECT * FROM goods where 11 <if test "gName!null"> and g.g_name like co…

数字孪生关键技术及体系架构

摘要&#xff1a; 数字孪生以各领域日益庞大的数据为基本要素&#xff0c;借助发展迅速的建模仿真、人工智能、虚拟现实等先进技术&#xff0c;构建物理实体在虚拟空间中的数字孪生体&#xff0c;实现对物理实体的数字化管控与优化&#xff0c;开拓了企业数字化转型的可行思路…

【Effective Web】页面优化

页面优化 页面渲染流程 JavaScript 》 Style 》 Layout 》 Paint 》 Composite 首先js做了一些逻辑&#xff0c;触发了样式变化&#xff0c;style计算好这些变化后&#xff0c;把影响的dom元素进行重新布局&#xff08;layout&#xff09;,再画到画布中&#xff08;Paint&am…

Untiy 布局控制器Aspect Ratio Fitter

Aspect Ratio Fitter是Unity中的一种布局控制器组件&#xff0c;用于根据指定的宽高比来调整包含它的UI元素的大小。实际开发中&#xff0c;它可以确保UI元素保持特定的宽高比&#xff0c;无论UI元素的内容或父容器的大小如何变化。 如图为Aspect Ratio Fitter组件的基本属性&…

纯分享万岳外卖跑腿系统客户端源码uniapp目录结构示意图

系统买的是商业版&#xff0c;使用非常不错有三端uniapp开源代码&#xff0c;自从上次分享uniapp后有些网友让我分享下各个端的uniapp下的各个目录结构说明 我就截图说以下吧&#xff0c;

AI预测福彩3D第20弹【2024年3月28日预测--第5套算法开始计算第2次测试】

今天&#xff0c;咱们继续进行本套算法的测试&#xff0c;今天为第二次测试&#xff0c;仍旧是采用冷温热趋势结合AI模型进行预测。好了&#xff0c;废话不多说了。直接上结果~ 仍旧是分为两个方案&#xff0c;1大1小。 经过人工神经网络计算并进行权重赋值打分后&#xff0c;3…

探索数据库mysql--------------mysql主从复制和读写分离

目录 前言 为什么要主从复制&#xff1f; 主从复制谁复制谁&#xff1f; 数据放在什么地方&#xff1f; 一、mysql支持的复制类型 1.1STATEMENT&#xff1a;基于语句的复制 1.2ROW&#xff1a;基于行的复制 1.3MIXED&#xff1a;混合类型的复制 二、主从复制的工作过程 三个重…