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://www.mzph.cn/news/777928.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用API有效率地管理Dynadot域名,使用API获得域名转移密码

关于Dynadot Dynadot​​​​​​​是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#x…

vue3+threejs新手从零开发卡牌游戏(十九):添加战斗事件

接上一节实现画线后&#xff0c;现在可以根据鼠标移动位置判断是否选中了对方区域怪兽卡牌&#xff1a; 修改game/index.vue代码&#xff0c;在画线方法中添加获取目标对象方法&#xff1a; const selectedCard ref() // 选中的场上card const selectedTargetCard ref() // …

【冥想X理工科思维】场景13:系统上线遭遇崩溃…

冥想音频合集&#xff1a;职场解压冥想音频 压力场景&#xff1a; 我搭建的系统刚刚在客户那边上线不到三天&#xff0c;系统就崩溃了&#xff0c;客户打电话来对我破口大骂&#xff0c;我该如何借助冥想调整面对客户时的压力&#xff1f; 点击看大图&#xff1a; 详细说明&…

从零开始的软件开发实战:互联网医院APP搭建详解

今天&#xff0c;笔者将以“从零开始的软件开发实战&#xff1a;互联网医院APP搭建详解”为主题&#xff0c;深入探讨互联网医院APP的开发过程和关键技术。 第一步&#xff1a;需求分析和规划 互联网医院APP的主要功能包括在线挂号、医生预约、医疗咨询、健康档案管理等。我们…

uniApp使用XR-Frame创建3D场景(7)加入点击交互

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用 这篇我们讲解如何与场景中的模型交互&#xff08;点击识别&#xff09; 先看源码 <xr-scene render-system"alpha:true" bind:ready"handleReady"><xr-node><xr-mesh id"…

网络socket编程(一)——socket接口函数、面向数据报的UDP编程及测试、select函数应用

目录 一、socket简介 二、socket编程接口函数介绍 2.1 socket()函数&#xff08;创建socket&#xff09; 2.2 bind()函数&#xff08;绑定地址和端口&#xff09; 2.3 listen()函数&#xff08;设置socket为监听模式&#xff09; 2.4 accept()函数&#xff08;接受连接…

jupyter notebook的各种问题和解决办法

安装jupyter&#xff0c;无法启动&#xff0c;或者经常crash 解决办法&#xff1a; 1,不要安装anaconda全家桶&#xff0c;速度慢&#xff0c;而且会安装另外一套python和库&#xff0c;导致代码跑不起来&#xff0c;容易crash。 2&#xff0c;直接安装jupyter&#xff1a; p…

八大技术趋势案例(人工智能物联网)

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

Spark-Scala语言实战(6)

在之前的文章中&#xff0c;我们学习了如何在scala中定义与使用类和对象&#xff0c;并做了几道例题。想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点赞&#xff0c;谢谢。 Spark-S…

HCIA-Datacom H12-811 题库补充(3/28)

完整题库及答案解析&#xff0c;请直接扫描上方二维码&#xff0c;持续更新中 OSPFv3使用哪个区域号标识骨干区域&#xff1f; A&#xff1a;0 B&#xff1a;3 C&#xff1a;1 D&#xff1a;2 答案&#xff1a;A 解析&#xff1a;AREA 号0就是骨干区域。 STP下游设备通知上游…

eplan部件清单的生成及自建部件库简介

生成报表页面: 右下图是报表页面,如果图纸有改动,点左图更新 举例,下图有急停按钮符号,要把急停按钮显示在清单里.第一步,已经有了符号(下左图急停),符号有设备标识符.第二步就是简易自建部件,第三部是在符号属性里关联自建部件 上图是右键点击同类型的部件,然后新建.下图新建部…

进阶了解C++(6)——二叉树OJ题

Leetcode.606.根据二叉树创建字符串&#xff1a; 606. 根据二叉树创建字符串 - 力扣&#xff08;LeetCode&#xff09; 难度不大&#xff0c;根据题目的描述&#xff0c;首先对二叉树进行一次前序遍历&#xff0c;即&#xff1a; class Solution { public:string tree2str(Tr…

jmeter总结之:Regular Expression Extractor元件

Regular Expression Extractor是一个后处理器元件&#xff0c;使用正则从服务器的响应中提取数据&#xff0c;并将这些数据保存到JMeter变量中&#xff0c;以便在后续的请求或断言中使用。在处理动态数据或验证响应中的特定信息时很有用。 添加Regular Expression Extractor元…

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

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

数据库原理与应用(SQL Server)笔记 关系数据库

目录 一、关系数据库的基本概念&#xff08;一&#xff09;关系数据库的定义&#xff08;二&#xff09;基本表、视图&#xff08;三&#xff09;元组、属性、域&#xff08;四&#xff09;候选码、主码、外码 二、关系模型三、关系的完整性&#xff08;一&#xff09;实体完整…

010——服务器开发环境搭建及开发方法(下)

目录 三、 第一个驱动程序 四、 buildroot 4.1 制作根文件系统 4.2 buildroot使用 五、 uboot 009——服务器开发环境搭建及开发方法&#xff08;上&#xff09;-CSDN博客 三、 第一个驱动程序 # 1. 使用不同的开发板内核时, 一定要修改KERN_DIR # 2. KERN_DIR中的内核要…

百度智能云推出AI大模型全家桶;抖音发布 AI 生成虚拟人物治理公告

百度智能云推出大模型全家桶 百度智能云昨日在北京首钢园召开「Al Cloud Day: 大模型应用产品发布会」&#xff0c;此次发布会上&#xff0c;百度智能云宣布对以下 7 款产品进行升级。 数字人平台百度智能云曦灵智能客服平台百度智能云客悦内容创作平台「一念」知识智平台「甄…

【计算机网络】第 11、12 问:流量控制和可靠传输机制有哪些?

目录 正文流量控制的基本方法停止-等待流量控制基本原理滑动窗口流量控制基本原理 可靠传输机制1. 停止-等待协议2. 后退 N 帧协议&#xff08;GBN&#xff09;3. 选择重传协议&#xff08;SR&#xff09; 正文 流量控制涉及对链路上的帧的发送速率的控制&#xff0c;以使接收…

HTTP

HTTP 概念&#xff1a;HyperTextTransferProtocol&#xff0c;超文本传输协议&#xff0c;规定了浏览器和服务器之间数据传输的规则 HTTP协议特点&#xff1a; 1.基于TCP协议&#xff1a;面向连接&#xff0c;安全 2.基于请求-响应模型的&#xff1a;一次请求对应一次响应 …

区块链dapp开发 dapp系统开发方案

在区块链技术的兴起和普及的推动下&#xff0c;去中心化应用程序&#xff08;DApp&#xff09;成为了当前数字世界中的热门话题之一。DApp 的开发不仅需要考虑技术方面的挑战&#xff0c;还需要深入了解区块链的工作原理和应用场景。本文将介绍一种 DApp 系统开发的基本方案&am…