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.rhkb.cn/news/289752.html

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

相关文章

<QT基础(5)>事件监听

事件监听 事件监听&#xff08;Event Handling&#xff09;是在程序中监视和响应发生的事件的一种机制。在Qt中&#xff0c;事件监听是一种常见的用于处理用户输入、系统事件以及其他类型事件的方法。通过事件监听&#xff0c;您可以在发生特定事件时捕获事件并执行相应的操作…

设计模式 —— 设计原则

在软件开发中&#xff0c;为了提高软件系统的可维护性和可复用性&#xff0c;增加软件的可扩展性和灵活性&#xff0c;程序员要尽量根据6条原则来开发程序&#xff0c;从而提高软件开发效率、节约软件开发成本和维护成本。 开闭原则 对扩展开放&#xff0c;对修改关闭。在程序需…

【MagicDrive环境配置】新手配俩星期版

1.创建一个新的环境conda create -n newdrive python3.8 2.激活该环境conda activate newdrive 3.下载MagicDrive源码 git clone --recursive https://github.com/cure-lab/MagicDrive.git&#xff0c;如果出现时间超时八成是网的问题&#xff0c;直接自己下载解压就好 3.我的…

Spring高级面试题-2024

Spring 框架中都用到了哪些设计模式&#xff1f; 1. 简单工厂&#xff1a; ○ BeanFactory&#xff1a;Spring的BeanFactory充当工厂&#xff0c;负责根据配置信息创建Bean实例。它是一种工厂模式的应用&#xff0c;根据指定的类名或ID创建Bean对象。2. 工厂方法&#xff…

华为防火墙配置指引超详细(包含安全配置部分)以USG6320为例

华为防火墙USG6320 华为防火墙USG6320是一款高性能、高可靠的下一代防火墙,适用于中小型企业、分支机构等场景。该防火墙支持多种安全功能,可以有效抵御网络攻击,保护网络安全。 目录 华为防火墙USG6320 1. 初始配置 2. 安全策略配置 3. 防火墙功能配置 4. 高可用性配…

【scala】使用gradle和scala构建springboot程序

零、版本说明: springboot: 2.7.18 使用log4j2&#xff0c;不使用springboot自带的logback scala版本&#xff1a;2.11 jackson版本&#xff1a;2.16.0 一、依赖&#xff1a; buildscript {dependencies {// using spring-boot-maven-plugin as package toolclasspath("…

前缀和与差分

前缀和 使用一个数组sum来维护原数组a的前缀和&#xff0c;即sum[i] a[1] a[2] ... a[i] 前缀和其实非常简单&#xff0c;它的用处也无处不在。最主要的进行多次的区间求和&#xff0c;会在很多其他的算法中出现。 例如&#xff1a;求a[l...r]的和&#xff0c;即sum[r] - …

最小可行产品需要最小可行架构——可持续架构(三)

前言 最小可行产品&#xff08;MVP&#xff09;的概念可以帮助团队专注于尽快交付他们认为对客户最有价值的东西&#xff0c;以便在投入大量时间和资源之前迅速、廉价地评估产品的市场规模。MVP不仅需要考虑产品的市场可行性&#xff0c;还需要考虑其技术可行性&#xff0c;以…

【教程】iOS如何抓取HTTP和HTTPS数据包经验分享

&#x1f4f1; 在日常的App开发和研发调研中&#xff0c;对各类App进行深入的研究分析时&#xff0c;我们需要借助专业的抓包应用来协助工作。本文将介绍如何使用iOS手机抓包工具来获取HTTP和HTTPS数据包&#xff0c;并推荐一款实用的抓包应用——克魔助手&#xff0c;希望能够…

【PyQt】18 -菜单等顶层操作

顶层界面的使用 前言一、菜单栏1.1 代码1.2 运行结果 二、工具栏2.1 代码几种显示方法 2.2 运行结果 三、状态栏3.1 代码3.2 运行结果 总结 前言 1、介绍顶层菜单栏目的使用&#xff0c;但没有陆续绑定槽函数。 2、工具栏 3、状态栏 一、菜单栏 1.1 代码 #Author &#xff1a…

插入排序、归并排序、堆排序和快速排序的稳定性分析

插入排序、归并排序、堆排序和快速排序的稳定性分析 一、插入排序的稳定性二、归并排序的稳定性三、堆排序的稳定性四、快速排序的稳定性总结在计算机科学中,排序是将一组数据按照特定顺序进行排列的过程。排序算法的效率和稳定性是评价其优劣的两个重要指标。稳定性指的是在排…

k8s 如何获取加入节点命名

当k8s集群初始化成功的时候&#xff0c;就会出现 加入节点 的命令如下&#xff1a; 但是如果忘记了就需要找回这条命令了。 kubeadm join 的命令格式如下&#xff1a;kubeadm join --token <token> --discovery-token-ca-cert-hash sha256:<hash>--token 令牌--…

【Linux】详解进程程序替换

一、替换原理 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支)&#xff0c;子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时&#xff0c;该进程的用户空间代码和数据完全被新程序替换&#xff0c;从新程序的启动例程开始执…

UDP send 出现大量“Resource temporarily unavailable”

背景 最近排查用户现场环境&#xff0c;查看日志出现大量的“send: Resource temporarily unavailable”错误&#xff0c;UDP设置NO_BLOCK模式&#xff0c;send又发生在进程上下文&#xff0c;并且还设置了SO_SNDBUF 为8M&#xff0c;在此情况下为什么还会出现发送队列满的情况…

Grafana+Promethues配置RocketMQ监控

背景 接前文&#xff0c;Promethues已经配置完毕&#xff0c;下面通过导入的Grafana的面板来配置RocketMQ监控页面 Dashboard 这里我们直接使用Grafana现成的面板配置 node_exporter&#xff1a;https://grafana.com/grafana/dashboards/1860 rocketmq_exporter的dashboar…

基于ssm的线上旅行信息管理系统论文

摘 要 随着旅游业的迅速发展&#xff0c;传统的旅行信息查询管理方式&#xff0c;已经无法满足用户需求&#xff0c;因此&#xff0c;结合计算机技术的优势和普及&#xff0c;特开发了本线上旅行信息管理系统。 本论文首先对线上旅行信息管理系统进行需求分析&#xff0c;从系…

网络工程师实验命令(华为数通HCIA)

VRP系统的基本操作 dis version #查看设备版本信息 sys #进入系统视图 system-name R1 #改设备名字为R1进入接口配置IP地址 int g0/0/0 ip address 192.168.1.1 255.255.255.0 #配置接口地址为192.168.1.1/255.255.255.0 ip address 192.168.1.2 24 sub #此…

基于Spring Boot 3 + Spring Security6 + JWT + Redis实现登录、token身份认证

基于Spring Boot3实现Spring Security6 JWT Redis实现登录、token身份认证。 用户从数据库中获取。使用RESTFul风格的APi进行登录。使用JWT生成token。使用Redis进行登录过期判断。所有的工具类和数据结构在源码中都有。 系列文章指路&#x1f449; 系列文章-基于Vue3创建前端…

小程序利用WebService跟asp.net交互过程发现的问题并处理

最近在研究一个项目&#xff0c;用到asp.net跟小程序交互&#xff0c;简单的说就是小程序端利用wx.request发起请求。获取asp.net 响应回来的数据。但经常会报错。点击下图的测试按钮 出现如下错误&#xff1a; 百思不得其解&#xff0c;试了若干方法&#xff0c;都不行。 因为…

ChatGPT 商业金矿(上)

原文&#xff1a;ChatGPT Business Goldmines 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第一章&#xff1a;为什么我写这本书 欢迎阅读《ChatGPT 多源收入&#xff1a;20 个利润丰厚的业务&#xff0c;任何人都可以在一周内使用 ChatGPT 开始》。我很高兴分享我…