【Flutter技术分享】ScrollMetricsNotification的诞生记

0 前言

ScrollMetricsNotification是Flutter2.5版本引入的一个新特性(#85221#85499),这个特性也作为了当时的亮点特性对外展示,它的加入,使得Flutter可滚动组件的滚动信息通知机制更加完善,开发者可以订阅任何时期、任何原因引起的可滚动组件的ScrollMetrics变化。

我们先来看看官方文档ScrollMetricsNotification介绍:

A notification that a scrollable widget's ScrollMetrics have changed.

For example, when the content of a scrollable is altered, making it larger or smaller, this notification will be dispatched. Similarly, if the size of the window or parent changes, the scrollable can notify of these changes in dimensions.

The above behaviors usually do not trigger ScrollNotification events, so this is useful for listening to ScrollMetrics changes that are not caused by the user scrolling.

ScrollMetricsNotification引入之前,开发者只能通过订阅ScrollNotification来感知滚动事件。我们来看一下ScrollNotification的限制:

1 ScrollNotification的使用限制

  • 不能够通知Scrollable的初始滚动状态;
  • 只通知由用户滚动触发的状态改变,由于内容动态变化、Viewport尺寸变化等在Layout阶段引起的ScrollMetrics都不会触发通知;

以上大大限制了开发者的使用场景,特别是当Flutter正式支持web、Desktop平台之后,这样的使用场景越来越多,社区也收到越来越多的缺陷和需求反馈,比较典型的问题有:

  • #67690 Scrollbar在web上表现异常;
  • #75613 ScrollController.position的监听在内容发生变化的时候没有收到回调;

#67690 问题的根因是Scrollbar通过订阅ScrollNotification来感知所关联的可滚动组件的滚动状态,然而,当通过window resize来改变Viewport的尺寸的时候并没有触发任何通知来刷新Scrollbar重绘;

#75613 问题的根因是在Layout阶段(长列表Lazy Loading发生在Flutter的Layout阶段),由于长列表内容发生变化导致的滚动状态变化没有一种通知机制导致;

可以看出,以上问题均是由于Flutter框架的限制导致,越来越多的应用场景需要感知Layout阶段、非用户滚动等原因导致的ScrollMetrics变化通知,于是ScrollMetricsNotification就应运而生。

2 ScrollMetricsNotification的实现原理

以上我们讲到了ScrollNotification的限制均是layout阶段引发的ScrollMetrics,于是,我们就从RenderViewpot.performLayout()着手分析:

dart 复制代码
    do {
      correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
      if (correction != 0.0) {
        offset.correctBy(correction);
      } else {
        if (offset.applyContentDimensions(
              math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
              math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
           )) {
          break;
        }
      }
      count += 1;
    } while (count < _maxLayoutCycles);

Viewportlayout完成之后会调用offset.applyContentDimensions:

dart 复制代码
    if (_isMetricsChanged()) {
      // It is too late to send useful notifications, because the potential
      // listeners have, by definition, already been built this frame. To make
      // sure the notification is sent at all, we delay it until after the frame
      // is complete.
      if (!_haveScheduledUpdateNotification) {
        scheduleMicrotask(didUpdateScrollMetrics);
        _haveScheduledUpdateNotification = true;
      }
      _lastMetrics = copyWith();
    }

这里就是ScrollMetricsNotification处理的核心代码了,代码的核心逻辑如下:

  1. 新增成员变量_lastMetrics初始值为null,如果本次layout后的ScrollMetrics发生了变化,考虑触发变化通知,我们来看下_isMetricsChanged()的处理逻辑:
dart 复制代码
  bool _isMetricsChanged() {
    assert(haveDimensions);
    final ScrollMetrics currentMetrics = copyWith();  // 获取当前metrics

    return _lastMetrics == null ||  // _lastMetrics为空代表初始态第一次layout
           !(currentMetrics.extentBefore == _lastMetrics!.extentBefore &&
             currentMetrics.extentInside == _lastMetrics!.extentInside &&
             currentMetrics.extentAfter == _lastMetrics!.extentAfter &&
             currentMetrics.axisDirection == _lastMetrics!.axisDirection);
  }

以上逻辑可以看出,如果_lastMetricsnull或者发生变化的时候,则返回true

  1. 如果_isMetricsChanged()返回true,则通过scheduleMicrotask 异步发生通知事件,这里很重要,大家思考下为什么要通过它来异步触发通知事件?答案就在这段注释里,我们来看一下:
less 复制代码
  // It is too late to send useful notifications, because the potential
  // listeners have, by definition, already been built this frame. To make
  // sure the notification is sent at all, we delay it until after the frame
  // is complete.

主要原因就是当前处于Flutter的layout阶段,如果发送通知并且开发者在回调处理函数中有setState操作会触发框架的断言,不允许在非build阶段调用setState

_haveScheduledUpdateNotification确保每一帧只发送一次变化通知。

以上逻辑就完美解决了ScrollNotification两点限制,有了ScrollMetricsNotification通知之后,开发者就可能结合两者一起使用,就能搞感知在任何时间、任何原因引起的ScrollMetrics的变化了,需要注意的是,ScrollMetricsNotification是对ScrollNotification的一种补充,而不是替代关系,大部分场景需要开发者结合两个通知一起使用,当然Flutter也给大家封装了一个类ScrollNotificationObserver,能够一次监听这两种通知消息。具体实现原理就是通过ScrollUpdateNotification.asScrollUpdate()ScrollMetricsNotification转换成了ScrollUpdateNotification类型。

3 总结

以上我们分享了ScrollMetricsNotification的实现原理和应用场景、以及与ScrollNotification异同,如果你对相关领域还有疑问和诉求,欢迎与作者沟通:)

作者长期活跃在Flutter开源社区,欢迎大家一起参与开源社区的共建,如果您也有意愿参与Flutter社区的贡献,可以与作者联系。-->GITHUB

您也许还对这些Flutter技术分享感兴趣:

  1. 《社区说|从Flutter Key 深入剖析UI架构设计原理》
  2. 《社区说|Flutter 长列表 Lazy Loading 机制解析》
相关推荐
sunly_9 小时前
Flutter:父组件,向子组件传值,子组件向二级页面传值
flutter
爱学习的绿叶13 小时前
flutter TabBarView 动态添加删除页面
flutter
趴菜小玩家15 小时前
使用 Gradle 插件优化 Flutter Android 插件开发中的 Flutter 依赖缺失问题
android·flutter·gradle
jhonjson1 天前
Flutter开发之flutter_local_notifications
flutter·macos·cocoa
iFlyCai2 天前
23种设计模式的Flutter实现第一篇创建型模式(一)
flutter·设计模式·dart
恋猫de小郭2 天前
Flutter 小技巧之 OverlayPortal 实现自限性和可共享的页面图层
flutter
A_cot2 天前
Vue.js:构建现代 Web 应用的强大框架
前端·javascript·vue.js·flutter·html·web·js
B.-2 天前
在 Flutter 应用中调用后端接口的方法
android·flutter·http·ios·https
️ 邪神2 天前
【Android、IOS、Flutter、鸿蒙、ReactNative 】约束布局
android·flutter·ios·鸿蒙·reactnative
pinkrecall20122 天前
flutter调试
flutter