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.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、商家中心等产品形态,通过数字化工具,实现门店的用户上平台、交互上平台、交易上平台、交付上平台,从而助力海尔专卖店的零售转型,并实现三翼鸟店的场景创新。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

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

解码“零信任”,如何带来信任感?

零信任的“信任”来源&#xff0c;并非凭空而生&#xff0c;而是建立在严格、细致且持续的验证、策略之上。它不仅能够提升企业的安全防护能力&#xff0c;也在加速安全技术的创新与演进。 推动创新 零信任理念激活网络安全 身份和访问管理革新。零信任理念“永不信任&#…

内存泄露排查流程

一、创建内存泄露案例 package com.mxl.controller;import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.Re…

asp.net开发中小程序端跟后端交互中的发现

小程序端wxml端代码示例&#xff1a; <button bind:tap"test">提交</button>小程序端js代码示例&#xff1a; test(){console.log(ok)wx.request({url: https://localhost:44375/lianxi01.aspx,})},asp.net端代码示例&#xff1a; cs端代码示例&#x…

人工智能与大数据、云计算等其他技术的关联和区别是什么?

人工智能&#xff08;AI&#xff09;、大数据和云计算是当今科技领域的三大热门技术&#xff0c;它们之间存在密切的关联&#xff0c;但也有一些明显的区别。以下是它们之间的关联和区别&#xff1a; AI-321 | 专注于AI工具分享的网站 AI工具集 | 人工智能工具箱 | 全球顶尖AI…

Eclipse+Java+Swing实现斗地主游戏

一. 视频演示效果 java斗地主源码演示 ​ 二.项目结构 代码十分简洁&#xff0c;只有简单的7个类&#xff0c;实现了人机对战 素材为若干的gif图片 三.项目实现 启动类为Main类&#xff0c;继承之JFrame&#xff0c;JFrame 是 Java Swing 库中的一个类&#xff0c;用于创建窗…

HarmonyOS入门--ArkTS--基本语法

文章目录 ArkTSArkTS声明式开发范式的基本组成基本语法声明式UI创建组件配置属性配置事件配置子组件 自定义组件基本结构成员函数/变量build()函数自定义组件通用样式自定义组件的创建和渲染流程自定义组件重新渲染自定义组件的删除 Builder装饰器全局自定义构建函数组件内部的…

k8s安装traefik作为ingress

一、先来介绍下Ingress Ingress 这个东西是 1.2 后才出现的&#xff0c;通过 Ingress 用户可以实现使用 nginx 等开源的反向代理负载均衡器实现对外暴露服务&#xff0c;以下详细说一下 Ingress&#xff0c;毕竟 traefik 用的就是 Ingress 使用 Ingress 时一般会有三个组件: …

如何调试Clang源码

下载编译Clang 这个就直接去LLVM官网下载&#xff0c;然后编译好Clang就行&#xff0c;注意得debug模式&#xff0c;保存符号信息。 调试Clang 可以直接通过命令行来调试 #进入调试环境&#xff0c;这里的clang得是刚刚编译好的 lldb ./clang # r是运行&#xff0c;后面是正…

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

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

学点Java_Day12_JDBC

1 JDBC 面向接口编程 在JDBC里面Java这个公司只是提供了一套接口Connection、Statement、ResultSet&#xff0c;每个数据库厂商实现了这套接口&#xff0c;例如MySql公司实现了&#xff1a;MySql驱动程序里面实现了这套接口&#xff0c;Java程序员只要调用实现了这些方法就可以…

零基础10 天入门 Web3之第1天

10 天入门 Web3 Web3 是互联网的下一代&#xff0c;它将使人们拥有自己的数据并控制自己的在线体验。Web3 基于区块链技术&#xff0c;该技术为安全、透明和可信的交易提供支持。我准备做一个 10 天的学习计划&#xff0c;可帮助大家入门 Web3&#xff1a; 想要一起探讨学习的…

如何使用OpenHarmony实现视频暂停、播放、切换、倍速播放

介绍 本篇Codelab使用ArkTS语言实现视频播放器&#xff0c;主要包括主页面和视频播放页面&#xff0c;我们将一起完成以下功能&#xff1a; 获取本地视频和网络视频。通过AVPlayer进行视频播放。通过手势调节屏幕亮度和视频播放音量。 相关概念 AVPlayer&#xff1a;播放管理…

CDH集群hive初始化元数据库失败

oracle数据库操作&#xff1a; 报错如下&#xff1a;命令 (Validate Hive Metastore schema (237)) 已失败 截图如下&#xff1a; 后台日志部分摘录&#xff1a; WARNING: Use “yarn jar” to launch YARN applications. SLF4J: Class path contains multiple SLF4J binding…

鸿鹄工程项目管理系统源码:Spring Boot带来的快速开发与部署体验

工程项目管理涉及众多环节和角色&#xff0c;如何实现高效协同和信息共享是关键。本文将介绍一个采用先进技术框架的Java版工程项目管理系统&#xff0c;该系统支持前后端分离&#xff0c;功能全面&#xff0c;可满足不同角色的需求。从项目进度图表到施工地图&#xff0c;再到…

Spark-Scala语言实战(6)

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

设计模式之外观模式解析

外观模式 1&#xff09;概述 1.问题 在软件开发中&#xff0c;为完成一项较为复杂的功能&#xff0c;一个客户类需要和多个业务类交互&#xff0c;而这些需要交互的业务类经常会作为一个整体出现&#xff0c;由于涉及到的类比较多&#xff0c;导致使用时代码较为复杂。 2.作…

TitanIDE与传统 IDE 比较

与传统IDE的比较 TitanIDE 和传统 IDE 属于不同时代的产物&#xff0c;在手工作坊时代&#xff0c;一切都是那么的自然&#xff0c;开发者习惯 Windows 或 MacOS 原生 IDE。不过&#xff0c;随着时代的变迁&#xff0c;软件行业已经步入云原生时代&#xff0c;TitanIDE 是顺应…

[flink] flink macm1pro 快速使用从零到一

文章目录 快速使用 快速使用 打开 https://flink.apache.org/downloads/ 下载 flink 因为书籍介绍的是 1.12版本的&#xff0c;为避免不必要的问题&#xff0c;下载相同版本 解压 tar -xzvf flink-1.11.2-bin-scala_2.11.tgz启动 flink ./bin/start-cluster.sh打开 flink web…

admin端

一、创建项目 1.1 技术栈 1.2 vite 项目初始化 npm init vitelatest vue3-element-admin --template vue-ts 1.3 src 路径别名配置 Vite 配置 配置 vite.config.ts // https://vitejs.dev/config/import { UserConfig, ConfigEnv, loadEnv, defineConfig } from vite im…