Flutter 中的 ScrollNotification 为啥收不到

news2024/12/22 18:08:51

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.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  
        return false;  
    }  
  }  
  return true;  
}
/// 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) {
      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 notification
    return 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(
                          /// 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}');
          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.coloradmin.cn/o/1552561.html

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

相关文章

STM32实现三个串口同时开启发送接收数据

程序目的&#xff1a; 实现STM32开通三个串口&#xff0c;每个串口都可以实现接收和发送数据。 注意事项&#xff1a; 编程时&#xff0c;严禁在中断函数中写入发送串口数据代码&#xff0c;否则会出错&#xff0c;具体原因不清楚&#xff08;有大佬知道的话帮我指出&#xff…

C#String的remove的用法

string test "abc";string temp test;temp.Remove(0, 1);temp temp.Remove(0, 1);Console.WriteLine(temp);Console.WriteLine(test);执行结果

UE RPC 外网联机(1)

技术&#xff1a;RPC TCP通信 设计&#xff1a;大厅服务<---TCP--->房间服务<---RPC--->客户端&#xff08;Creator / Participator&#xff09; 1. PlayerController 用于RPC通信控制 2.GameMode 用于数据同步 3.类图 4. 注意 &#xff08;1&#xff09;RPC&a…

uniapp h5 touch事件踩坑记录

场景&#xff1a;悬浮球功能 当我给悬浮球设置了 position: fixed; 然后监听悬浮球的touch事件&#xff0c;从事件对象中拿到clientY和clientX赋值给悬浮球的left和top属性。当直接赋值后效果应该是这样子&#xff1a; 注意鼠标相对悬浮球的位置&#xff0c;应该就是左上角&a…

深度学习论文: Attention is All You Need及其PyTorch实现

深度学习论文: Attention is All You Need及其PyTorch实现 Attention is All You Need PDF:https://arxiv.org/abs/1706.03762.pdf PyTorch: https://github.com/shanglianlm0525/PyTorch-Networks 大多数先进的神经序列转换模型采用编码器-解码器结构&#xff0c;其中编码器将…

力扣热门算法题 135. 分发糖果,146. LRU 缓存,148. 排序链表

135. 分发糖果&#xff0c;146. LRU 缓存&#xff0c;148. 排序链表&#xff0c;每题做详细思路梳理&#xff0c;配套Python&Java双语代码&#xff0c; 2024.03.28 可通过leetcode所有测试用例。 目录 135. 分发糖果 解题思路 完整代码 Python Java 146. LRU 缓存 …

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…

|行业洞察·手机|《2024手机行业及营销趋势报告-18页》

报告的主要内容解读&#xff1a; 手机行业概述及品牌分布&#xff1a; 2022年&#xff0c;受疫情影响&#xff0c;中国国内手机市场出货量下降22.6%&#xff0c;总计2.72亿部。5G手机市场占有率中&#xff0c;苹果领先&#xff0c;其次是vivo、OPPO和华为。消费者换机时更注重性…

【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.我的…

【群智能算法改进】一种改进的同核分子优化算法 IHMO算法【Matlab代码#71】

文章目录 【获取资源请见文章第5节&#xff1a;资源获取】1. 原始HMO算法2. 改进HMO算法2.1 改进的距离减小因子2.2 黄金正弦策略扰动 3. 部分代码展示4. 仿真结果展示5. 资源获取说明 【获取资源请见文章第5节&#xff1a;资源获取】 1. 原始HMO算法 同核分子优化算法&#x…

HarmonyOS 应用开发之UIAbility组件生命周期

概述 当用户打开、切换和返回到对应应用时&#xff0c;应用中的UIAbility实例会在其生命周期的不同状态之间转换。UIAbility类提供了一系列回调&#xff0c;通过这些回调可以知道当前UIAbility实例的某个状态发生改变&#xff0c;会经过UIAbility实例的创建和销毁&#xff0c;…

网络原理-传输层-UDP报文结构

本文介绍UDP报文 有很多友友搞不清楚UDP报文的详细结构还有TCP的详细结构,所以专门分开来讲 以免弄混. 首先我们先看一下整个UDP结构,让大家有一个全方面的认识 下面我们来详细解释UDP报 16位源端口号(本机):就是2字节大小,16个二进制位. 16位目的端口号(目的机):也是2字节…

如何构建一台机器学习服务器

系统信息 系统安装 系统安装这里就不再赘述&#xff0c;推荐使用ventory作为PE盘&#xff0c;来安装系统&#xff0c;这样方便快捷&#xff0c;可同时包含多个镜像&#xff0c;无需重复制作&#xff0c;需要注意的是在安装系统的时候需要手动进行分区&#xff0c;我们可以看一下…

uniApp使用XR-Frame创建3D场景(3)光源投影的运用。

上一篇讲解了如何在uniApp中创建xr-frame子组件并创建简单的3D场景。 这篇我们讲解光源在场景中的运用以及相关属性。 在子组件 xr-start的index.wxml文件中我们加入如下代码 <xr-scene render-system"alpha:true" bind:ready"handleReady"><xr…

睿尔曼超轻量仿人机械臂之复合机器人底盘介绍及接口调用

机器人移动平台是一个包含完整成熟的感知、认知和定位导航能力的轮式机器人底盘产品级平台&#xff0c;产品致力于为各行业细分市场的商用轮式服务机器人提供一站式移动机器人解决方案&#xff0c;让合作伙伴专注在核心业务/人机交互的实现。以下是我司产品双臂机器人以及复合升…

主机安全-德迅卫士

什么是主机安全&#xff1f; 主机安全&#xff0c;其核心内容包括安全应用交付系统、应用监管系统、操作系统安全增强系统和运维安全管控系统。它的具体功能是指保证主机在数据存储和处理的保密性、完整性&#xff0c;可用性&#xff0c;它包括硬件、固件、系统软件的自身安全&…

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下游设备通知上游…

蓝桥杯_day6

文章目录 不同路径不同路径II拿金币珠宝的最高价值 不同路径 【题目描述】 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为…

啥也不会的大学生看过来,这8步就能系统入门stm32单片机???

大家好&#xff0c;今天给大家介绍啥也不会的大学生看过来&#xff0c;这8步就能系统入门stm32单片机&#xff0c;文章末尾附有分享大家一个资料包&#xff0c;差不多150多G。里面学习内容、面经、项目都比较新也比较全&#xff01;可进群免费领取。 对于没有任何基础的大学生来…

GPT:多轮对话并搭建简单的聊天机器人

1 多轮对话 多轮对话能力至关重要&#xff0c;它不仅能深化交流&#xff0c;精准捕捉对方意图&#xff0c;还能促进有效沟通&#xff0c;增强理解。在智能客服、教育辅导等领域&#xff0c;多轮对话更是提升服务质量、增强用户体验的关键。 注意&#xff1a;大模型没有多轮对话…