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
通过订阅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);
在Viewport
layout完成之后会调用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
处理的核心代码了,代码的核心逻辑如下:
- 新增成员变量
_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);
}
以上逻辑可以看出,如果_lastMetrics
为null
或者发生变化的时候,则返回true
。
- 如果
_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技术分享感兴趣: