Flutter.源码分析ScrollViewflutter/packages/flutter/lib/src/widgets/scroll_view.dart/ScrollView
1. 类注释部分
/// 一个组合了 [Scrollable] 和 [Viewport] 的组件,用于在一个维度上创建一个可交互的滚动内容窗格。 /// /// 可滚动组件由三部分组成: /// /// 1. 一个 [Scrollable] 组件,它监听各种用户手势并实现滚动的交互设计。 /// 2. 一个视口组件,如 [Viewport] 或 [ShrinkWrappingViewport],它通过仅显示滚动视图内部的部分组件来实现滚动的视觉设计。 /// 3. 一个或多个 slivers,这些组件可以组合起来创建各种滚动效果,如列表、网格和展开的头部。 /// /// [ScrollView] 通过创建 [Scrollable] 和视口,并将创建 slivers 的任务委托给其子类,来协调这些部分。 /// /// 要了解更多关于 slivers 的信息,请参阅 [CustomScrollView.slivers]。 /// /// 要控制滚动视图的初始滚动偏移量,提供一个设置了 [ScrollController.initialScrollOffset] 属性的 [controller]。 /// /// 另请参阅: /// /// * [ListView],这是一个常用的 [ScrollView],显示一个滚动的、线性的子组件列表。 /// * [PageView],这是一个滚动的子组件列表,每个子组件都是视口的大小。 /// * [GridView],这是一个 [ScrollView],显示一个滚动的、二维的子组件数组。 /// * [CustomScrollView],这是一个 [ScrollView],使用 slivers 创建自定义滚动效果。 /// * [ScrollNotification] 和 [NotificationListener],它们可以用来观察滚动位置,而无需使用 [ScrollController]。 /// * [TwoDimensionalScrollView],这是一个类似的组件 [ScrollView],它在两个维度上滚动。 abstract class ScrollView extends StatelessWidget {
2. 构造方法部分
/// 创建一个可以滚动的组件。 /// /// 如果没有提供 [controller],则 [ScrollView.primary] 参数默认为垂直滚动视图的 true。如果 [primary] 明确设置为 true,则 [controller] 参数必须为 null。如果 [primary] 为 true,则将最近的包围组件的 [PrimaryScrollController] 附加到此滚动视图。 /// /// 如果 [shrinkWrap] 参数为 true,则 [center] 参数必须为 null。 /// /// [scrollDirection]、[reverse] 和 [shrinkWrap] 参数必须不为 null。 /// /// [anchor] 参数必须为非 null,并且在 0.0 到 1.0 的范围内。 const ScrollView({ super.key, this.scrollDirection = Axis.vertical, this.reverse = false, this.controller, this.primary, ScrollPhysics? physics, this.scrollBehavior, this.shrinkWrap = false, this.center, this.anchor = 0.0, this.cacheExtent, this.semanticChildCount, this.dragStartBehavior = DragStartBehavior.start, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, this.clipBehavior = Clip.hardEdge, }) : assert( !(controller != null && (primary ?? false)), 'Primary ScrollViews obtain their ScrollController via inheritance ' 'from a PrimaryScrollController widget. You cannot both set primary to ' 'true and pass an explicit controller.', ), assert(!shrinkWrap || center == null), assert(anchor >= 0.0 && anchor <= 1.0), assert(semanticChildCount == null || semanticChildCount >= 0), physics = physics ?? ((primary ?? false) || (primary == null && controller == null && identical(scrollDirection, Axis.vertical)) ? const AlwaysScrollableScrollPhysics() : null);
3. scrollDirection 属性部分
/// {@template flutter.widgets.scroll_view.scrollDirection} /// 滚动视图偏移量增加的 [Axis]。 /// /// 对于可能发生活动滚动的方向,请参见 [ScrollDirection]。 /// /// 默认为 [Axis.vertical]。 /// {@endtemplate} final Axis scrollDirection;
4. reverse 属性部分
/// {@template flutter.widgets.scroll_view.reverse} /// 滚动视图是否按阅读方向滚动。 /// /// 例如,如果阅读方向是从左到右,且 [scrollDirection] 为 [Axis.horizontal], /// 那么当 [reverse] 为 false 时,滚动视图从左向右滚动,当 [reverse] 为 true 时,从右向左滚动。 /// /// 类似地,如果 [scrollDirection] 为 [Axis.vertical],那么当 [reverse] 为 false 时, /// 滚动视图从上向下滚动,当 [reverse] 为 true 时,从下向上滚动。 /// /// 默认为 false。 /// {@endtemplate} final bool reverse;
5. controller 属性部分
/// {@template flutter.widgets.scroll_view.controller} /// 可用于控制滚动视图滚动到哪个位置的对象。 /// /// 如果 [primary] 为 true,则必须为 null。 /// /// [ScrollController] 有多个用途。它可以用来控制初始滚动位置(参见 [ScrollController.initialScrollOffset])。 /// 它可以用来控制滚动视图是否应自动在 [PageStorage] 中保存和恢复其滚动位置(参见 [ScrollController.keepScrollOffset])。 /// 它可以用来读取当前滚动位置(参见 [ScrollController.offset]),或改变它(参见 [ScrollController.animateTo])。 /// {@endtemplate} final ScrollController? controller;
6. primary属性部分
/// {@template flutter.widgets.scroll_view.primary} /// 是否是与父 [PrimaryScrollController] 关联的主滚动视图。 /// /// 当此值为 true 时,即使滚动视图没有足够的内容实际滚动,也可以滚动。否则,默认情况下,用户只有在视图有足够的内容时才能滚动。参见 [physics]。 /// /// 同样,当为 true 时,滚动视图用于默认的 [ScrollAction]。如果 ScrollAction 没有被应用程序的其他聚焦部分处理, /// 则将使用此滚动视图评估 ScrollAction,例如,执行 [Shortcuts] 键事件,如页面上下。 /// /// 在 iOS 上,这还标识了将响应状态栏点击而滚动到顶部的滚动视图。 /// /// 不能在提供 `controller` 的 [ScrollController] 时为 true,只有一个 ScrollController 可以与 ScrollView 关联。 /// /// 设置为 false 将明确阻止继承任何 [PrimaryScrollController]。 /// /// 默认为 null。当为 null,且没有提供控制器时,使用 [PrimaryScrollController.shouldInherit] 决定自动继承。 /// /// 默认情况下,每个 [ModalRoute] 注入的 [PrimaryScrollController] 都配置为在 [TargetPlatformVariant.mobile] 上自动继承 /// [Axis.vertical] 滚动方向的 ScrollViews。在您的应用中添加另一个将覆盖其上方的 PrimaryScrollController。 /// /// 以下视频包含有关滚动控制器、PrimaryScrollController 组件及其对您的应用的影响的更多信息: /// /// {@youtube 560 315 https://www.youtube.com/watch?v=33_0ABjFJUU} /// /// {@endtemplate} final bool? primary;
从注释中可以了解到:
primary 属性决定了 ScrollView 是否是与父 PrimaryScrollController 关联的主滚动视图。
当 primary 属性为 true 时,即使滚动视图没有足够的内容可以实际滚动,也可以滚动。否则,默认情况下,用户只有在视图有足够的内容时才能滚动。
此外,当 primary 为 true 时,滚动视图用于默认的 ScrollAction。如果 ScrollAction 没有被应用程序的其他聚焦部分处理,那么将使用此滚动视图评估 ScrollAction,例如,执行 Shortcuts 键事件,如页面上下。
在 iOS 上,primary 为 true 还标识了将响应状态栏点击而滚动到顶部的滚动视图。
注意,不能在提供 controller 的 ScrollController 时将 primary 设置为 true,因为只有一个 ScrollController 可以与 ScrollView 关联。
设置 primary 为 false 将明确阻止继承任何 PrimaryScrollController。
primary 的默认值为 null。当 primary 为 null,且没有提供控制器时,将使用 PrimaryScrollController.shouldInherit 决定是否自动继承。
默认情况下,每个 ModalRoute 注入的 PrimaryScrollController 都配置为在 TargetPlatformVariant.mobile 上自动继承 Axis.vertical 滚动方向的 ScrollViews。在您的应用中添加另一个 PrimaryScrollController 将覆盖其上方的 PrimaryScrollController。
7. physics属性部分
/// {@template flutter.widgets.scroll_view.physics} /// 滚动视图应如何响应用户输入。 /// /// 例如,确定用户停止拖动滚动视图后,滚动视图如何继续动画。 /// /// 默认为匹配平台约定。此外,如果 [primary] 为 false,那么用户只有在有足够的内容可以滚动时才能滚动, /// 而如果 [primary] 为 true,他们总是可以尝试滚动。 /// /// 要强制滚动视图始终可以滚动,即使没有足够的内容,就像 [primary] 为 true 一样,但不一定要将其设置为 true, /// 提供一个 [AlwaysScrollableScrollPhysics] 物理对象,如下所示: /// /// ```dart /// physics: const AlwaysScrollableScrollPhysics(), /// ``` /// /// /// 要强制滚动视图使用默认的平台约定,并且如果内容不足,无论 [primary] 的值如何,都不可滚动, /// 提供一个明确的 [ScrollPhysics] 对象,如下所示: /// /// ```dart /// physics: const ScrollPhysics(), /// ``` /// /// 物理可以动态地改变(通过在后续的构建中提供一个新的对象),但新的物理只有在提供的对象的 _类_ 改变时才会生效。 /// 仅仅构造一个具有不同配置的新实例是不足以使物理重新应用的。 (这是因为最终使用的对象是动态生成的, /// 这可能相对昂贵,如果每帧都预测性地创建这个对象以查看物理是否应该更新,那将是低效的。) /// {@endtemplate} /// /// 如果向 [scrollBehavior] 提供了明确的 [ScrollBehavior],那么该行为提供的 [ScrollPhysics] 将优先于 [physics]。 final ScrollPhysics? physics;
从注释可以了解:
physics
属性在 ScrollView 中控制滚动行为的物理特性,例如滚动速度、滚动方向、滚动是否会反弹等。
- 默认情况下,
physics
会根据平台(iOS 或 Android)来选择合适的滚动行为。如果primary
属性为false
,用户只有在内容足够多,足以滚动时才能滚动。如果primary
为true
,即使内容不足,用户也可以尝试滚动。 - 如果你想让 ScrollView 无论内容是否足够,都可以滚动,你可以设置
physics
为 AlwaysScrollableScrollPhysics。这种情况下,即使primary
不是true
,滚动视图也总是可以滚动。 - 如果你想让 ScrollView 严格按照平台约定进行滚动,即当内容不足时,无论
primary
的值如何,都不能滚动,你可以设置physics
为 ScrollPhysics。 physics
属性可以动态改变,但是只有当你提供的物理对象的类发生改变时,新的物理属性才会生效。这是因为物理对象的创建可能会有一定的开销,如果每一帧都创建新的物理对象来检查是否需要更新物理属性,可能会导致性能问题。- 如果你为
scrollBehavior
提供了一个 ScrollBehavior 对象,那么这个对象提供的 ScrollPhysics 会优先于 ScrollView 的physics
属性。
8. scrollBehavior属性部分
/// {@macro flutter.widgets.shadow.scrollBehavior} /// /// [ScrollBehavior] 也提供 [ScrollPhysics]。如果在 [physics] 中提供了明确的 [ScrollPhysics],它将优先, /// 然后是 [scrollBehavior],然后是继承的祖先 [ScrollBehavior]。 final ScrollBehavior? scrollBehavior;
9. shrinkWrap属性部分
/// {@template flutter.widgets.scroll_view.shrinkWrap} /// 滚动视图在 [scrollDirection] 中的范围是否应由正在查看的内容确定。 /// /// 如果滚动视图没有收缩包装,则滚动视图将扩展到 [scrollDirection] 中允许的最大大小。 /// 如果滚动视图在 [scrollDirection] 中的约束是无界的,则 [shrinkWrap] 必须为 true。 /// /// 收缩包装滚动视图的内容比扩展到允许的最大大小要昂贵得多,因为内容可以在滚动过程中扩展和收缩, /// 这意味着每当滚动位置改变时,都需要重新计算滚动视图的大小。 /// /// 默认为 false。 /// /// {@youtube 560 315 https://www.youtube.com/watch?v=LUqDNnv_dh0} /// {@endtemplate} final bool shrinkWrap;
10. center属性部分
/// [GrowthDirection.forward] 生长方向的第一个子元素。 /// /// [center] 之后的子元素将相对于 [center] 在由 [scrollDirection] 和 [reverse] 确定的 [AxisDirection] 中放置。 /// [center] 之前的子元素将相对于 [center] 放置在轴方向的相反方向。这使得 [center] 成为生长方向的拐点。 /// /// [center] 必须是 [buildSlivers] 构建的滑块之一的键。 /// /// 在 [ScrollView] 的内置子类中,只有 [CustomScrollView] 支持 [center]; /// 对于该类,给定的键必须是 [CustomScrollView.slivers] 列表中的滑块之一的键。 /// /// 大多数滚动视图默认按 [GrowthDirection.forward] 排序。 /// 更改 [ScrollView.anchor]、[ScrollView.center] 或两者的默认值,可以为滚动视图配置 [GrowthDirection.reverse]。 /// /// {@tool dartpad} /// 此示例显示了一个 [CustomScrollView],在 [AppBar.bottom] 中有 [Radio] 按钮, /// 可以改变 [AxisDirection] 来展示不同的配置。[CustomScrollView.anchor] 和 [CustomScrollView.center] /// 属性也被设置为使 0 滚动偏移位于视口的中间,[GrowthDirection.forward] 和 [GrowthDirection.reverse] /// 在两侧显示。共享 [CustomScrollView.center] 键的滑块位于 [CustomScrollView.anchor] 的位置。 /// /// ** 参见 examples/api/lib/rendering/growth_direction/growth_direction.0.dart 中的代码 ** /// {@end-tool} /// /// 另请参见: /// /// * [anchor],它控制 [center] 在视口中的对齐方式。 final Key? center;
11. anchor属性部分
/// {@template flutter.widgets.scroll_view.anchor} /// 零滚动偏移的相对位置。 /// /// 例如,如果 [anchor] 是 0.5,由 [scrollDirection] 和 [reverse] 确定的 [AxisDirection] 是 [AxisDirection.down] 或 /// [AxisDirection.up],那么零滚动偏移在视口中垂直居中。如果 [anchor] 是 1.0,轴方向是 [AxisDirection.right], /// 那么零滚动偏移在视口的左边缘。 /// /// 大多数滚动视图默认按 [GrowthDirection.forward] 排序。 /// 更改 [ScrollView.anchor]、[ScrollView.center] 或两者的默认值,可以为滚动视图配置 [GrowthDirection.reverse]。 /// /// {@tool dartpad} /// 此示例显示了一个 [CustomScrollView],在 [AppBar.bottom] 中有 [Radio] 按钮, /// 可以改变 [AxisDirection] 来展示不同的配置。[CustomScrollView.anchor] 和 [CustomScrollView.center] /// 属性也被设置为使 0 滚动偏移位于视口的中间,[GrowthDirection.forward] 和 [GrowthDirection.reverse] /// 在两侧显示。共享 [CustomScrollView.center] 键的滑块位于 [CustomScrollView.anchor] 的位置。 /// /// ** 参见 examples/api/lib/rendering/growth_direction/growth_direction.0.dart 中的代码 ** /// {@end-tool} /// {@endtemplate} final double anchor;
12. cacheExtent属性部分
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent} final double? cacheExtent;
13. semanticChildCount属性部分
/// 将提供语义信息的子元素数量。 /// /// [ScrollView] 的一些子类型可以自动推断此值。例如 [ListView] 将使用子列表中的组件数量, /// 而 [ListView.separated] 构造函数将使用该数量的一半。 /// /// 对于 [CustomScrollView] 和其他类型,它们不接收构建器或组件列表,必须明确提供子计数。如果数量未知或无限,则应保留未设置或设置为 null。 /// /// 另请参见: /// /// * [SemanticsConfiguration.scrollChildCount],对应的语义属性。 final int? semanticChildCount;
14. dragStartBehavior属性部分
/// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior;
15. keyboardDismissBehavior属性部分
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} /// 定义此 [ScrollView] 如何自动消除键盘的 [ScrollViewKeyboardDismissBehavior]。 /// {@endtemplate} final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
16. restorationId属性部分
/// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId;
17. clipBehavior属性部分
/// {@macro flutter.material.Material.clipBehavior} /// /// 默认为 [Clip.hardEdge]。 final Clip clipBehavior;
18. getDirection方法部分
/// 返回滚动视图滚动的 [AxisDirection]。 /// /// 结合 [scrollDirection] 和 [reverse] 布尔值来获取具体的 [AxisDirection]。 /// /// 如果 [scrollDirection] 是 [Axis.horizontal],在选择具体的 [AxisDirection] 时也会考虑环境 [Directionality]。 /// 例如,如果环境 [Directionality] 是 [TextDirection.rtl],那么非反向的 [AxisDirection] 是 [AxisDirection.left], /// 反向的 [AxisDirection] 是 [AxisDirection.right]。 @protected AxisDirection getDirection(BuildContext context) { return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse); }
19. buildSlivers方法部分
/// 构建放置在视口内的组件列表。 /// /// 子类应重写此方法,以构建视口内部的滑块。 /// /// 要了解更多关于滑块的信息,请参见 [CustomScrollView.slivers]。 @protected List<Widget> buildSlivers(BuildContext context);
20. buildViewport方法部分
/// 构建视口(viewport)。 /// /// 子类可以重写此方法来改变视口的构建方式。如果 [shrinkWrap] 为 true,那么默认实现使用 [ShrinkWrappingViewport], /// 否则使用常规的 [Viewport]。 /// /// `offset` 参数是从 [Scrollable.viewportBuilder] 获取的值。 /// /// `axisDirection` 参数是从 [getDirection] 获取的值,该值默认使用 [scrollDirection] 和 [reverse]。 /// /// `slivers` 参数是从 [buildSlivers] 获取的值。 @protected Widget buildViewport( BuildContext context, ViewportOffset offset, AxisDirection axisDirection, List<Widget> slivers, ) { assert(() { switch (axisDirection) { case AxisDirection.up: case AxisDirection.down: return debugCheckHasDirectionality( context, // 为了确定滚动视图的交叉轴方向 why: 'to determine the cross-axis direction of the scroll view', // 垂直滚动视图创建试图从环境 Directionality 确定其交叉轴方向的 Viewport 组件。 hint: 'Vertical scroll views create Viewport widgets that try to determine their cross axis direction ' 'from the ambient Directionality.', ); case AxisDirection.left: case AxisDirection.right: return true; } }()); if (shrinkWrap) { return ShrinkWrappingViewport( axisDirection: axisDirection, offset: offset, slivers: slivers, clipBehavior: clipBehavior, ); } return Viewport( axisDirection: axisDirection, offset: offset, slivers: slivers, cacheExtent: cacheExtent, center: center, anchor: anchor, clipBehavior: clipBehavior, ); }
buildViewport 方法用于构建 ScrollView 的视口。
视口是 ScrollView 中可见的部分,它决定了用户在屏幕上看到的内容。视口内的内容可以滚动,而视口外的内容则不可见。
buildViewport
方法接收四个参数:context
、offset
、axisDirection
和 slivers
。
参数 | 描述 |
context | 是当前 BuildContext,它包含了当前 widget 的位置信息和状态 |
offset | 是从 Scrollable.viewportBuilder 获取的值,它表示当前滚动的位置 |
axisDirection | 是从 getDirection 方法获取的值,它表示滚动的方向。默认情况下,它使用 scrollDirection 和 reverse 属性来确定 |
slivers | 是从 buildSlivers 方法获取的值,它是一个 Widget 列表,表示视口内的内容 |
在 buildViewport
方法中:
- 首先会根据
axisDirection
的值进行一些断言检查,以确保滚动视图的交叉轴方向是正确的。 - 然后,如果
shrinkWrap
属性为true
,则使用 ShrinkWrappingViewport 来构建视口。 ShrinkWrappingViewport 是一种特殊的视口,它会根据其子组件的大小来调整自己的大小。 - 如果
shrinkWrap
属性为false
,则使用常规的 Viewport 来构建视口。Viewport 会尽可能地扩展到最大的可用空间。 - 最后,无论是 ShrinkWrappingViewport 还是 Viewport,都会使用传入的
axisDirection
、offset
和slivers
参数,以及 ScrollView 的clipBehavior
、cacheExtent
、center
和anchor
属性来进行构建。
21. build方法部分
@override Widget build(BuildContext context) { final List<Widget> slivers = buildSlivers(context); final AxisDirection axisDirection = getDirection(context); final bool effectivePrimary = primary ?? controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection); final ScrollController? scrollController = effectivePrimary ? PrimaryScrollController.maybeOf(context) : controller; final Scrollable scrollable = Scrollable( dragStartBehavior: dragStartBehavior, axisDirection: axisDirection, controller: scrollController, physics: physics, scrollBehavior: scrollBehavior, semanticChildCount: semanticChildCount, restorationId: restorationId, viewportBuilder: (BuildContext context, ViewportOffset offset) { return buildViewport(context, offset, axisDirection, slivers); }, clipBehavior: clipBehavior, ); final Widget scrollableResult = effectivePrimary && scrollController != null // Further descendant ScrollViews will not inherit the same PrimaryScrollController ? PrimaryScrollController.none(child: scrollable) : scrollable; if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { return NotificationListener<ScrollUpdateNotification>( child: scrollableResult, onNotification: (ScrollUpdateNotification notification) { final FocusScopeNode focusScope = FocusScope.of(context); if (notification.dragDetails != null && focusScope.hasFocus) { focusScope.unfocus(); } return false; }, ); } else { return scrollableResult; } }
ScrollView 组件的 build
方法中:
- 首先,它调用
buildSlivers
方法来构建视口内部的组件列表,然后调用getDirection
方法来获取滚动的方向。 - 接着,它确定是否使用主滚动控制器。如果
primary
属性为true
,或者没有提供controller
并且 PrimaryScrollController.shouldInherit 返回true
,那么effectivePrimary
就为true
。在这种情况下,滚动控制器scrollController
将使用 PrimaryScrollController.maybeOf(context) 获取,否则使用提供的controller
。 - 然后,它创建一个 Scrollable 组件,这个组件包含了滚动的所有信息,如滚动方向、滚动控制器、滚动物理等。
viewportBuilder
参数是一个函数,它返回视口组件,这个函数调用buildViewport
方法来构建视口。
如果effectivePrimary
为true
并且scrollController
不为null
,那么它会返回一个 PrimaryScrollController.none 组件,这样后代的 ScrollView 就不会继承同一个 PrimaryScrollController。否则,它直接返回 Scrollable 组件。 - 最后,如果
keyboardDismissBehavior
属性设置为 ScrollViewKeyboardDismissBehavior.onDrag,那么它会返回一个 NotificationListener 组件,这个组件会在滚动更新通知发生时取消焦点,从而隐藏键盘。否则,它直接返回 Scrollable 组件。
22. 其它代码
@override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(EnumProperty<Axis>('scrollDirection', scrollDirection)); properties.add(FlagProperty('reverse', value: reverse, ifTrue: 'reversed', showName: true)); properties.add(DiagnosticsProperty<ScrollController>('controller', controller, showName: false, defaultValue: null)); properties.add(FlagProperty('primary', value: primary, ifTrue: 'using primary controller', showName: true)); properties.add(DiagnosticsProperty<ScrollPhysics>('physics', physics, showName: false, defaultValue: null)); properties.add(FlagProperty('shrinkWrap', value: shrinkWrap, ifTrue: 'shrink-wrapping', showName: true)); }
debugFillProperties 方法是 Flutter 框架的一部分,用于在调试时提供有关 ScrollView 的信息。