Flutter.源码分析flutter/packages/flutter/lib/src/widg...ScrollView
本文提供 Flutter 框架中 ScrollView 类源码注释的中文翻译以及必要的分析解说。
- 1. 类注释部分
- 2. 构造方法部分
- 3. scrollDirection 属性部分
- 4. reverse 属性部分
- 5. controller 属性部分
- 6. primary属性部分
- 7. physics属性部分
- 8. scrollBehavior属性部分
- 9. shrinkWrap属性部分
- 10. center属性部分
- 11. anchor属性部分
- 12. cacheExtent属性部分
- 13. semanticChildCount属性部分
- 14. dragStartBehavior属性部分
- 15. keyboardDismissBehavior属性部分
- 16. restorationId属性部分
- 17. clipBehavior属性部分
- 18. getDirection方法部分
- 19. buildSlivers方法部分
- 20. buildViewport方法部分
- 21. build方法部分
- 22. 其它代码
/// 一个组合了 [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 {
/// 创建一个可以滚动的组件。
///
/// 如果没有提供 [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);
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// 滚动视图偏移量增加的 [Axis]。
///
/// 对于可能发生活动滚动的方向,请参见 [ScrollDirection]。
///
/// 默认为 [Axis.vertical]。
/// {@endtemplate}
final Axis scrollDirection;
/// {@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;
/// {@template flutter.widgets.scroll_view.controller}
/// 可用于控制滚动视图滚动到哪个位置的对象。
///
/// 如果 [primary] 为 true,则必须为 null。
///
/// [ScrollController] 有多个用途。它可以用来控制初始滚动位置(参见 [ScrollController.initialScrollOffset])。
/// 它可以用来控制滚动视图是否应自动在 [PageStorage] 中保存和恢复其滚动位置(参见 [ScrollController.keepScrollOffset])。
/// 它可以用来读取当前滚动位置(参见 [ScrollController.offset]),或改变它(参见 [ScrollController.animateTo])。
/// {@endtemplate}
final ScrollController? controller;
/// {@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。
/// {@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
属性。
/// {@macro flutter.widgets.shadow.scrollBehavior}
///
/// [ScrollBehavior] 也提供 [ScrollPhysics]。如果在 [physics] 中提供了明确的 [ScrollPhysics],它将优先,
/// 然后是 [scrollBehavior],然后是继承的祖先 [ScrollBehavior]。
final ScrollBehavior? scrollBehavior;
/// {@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;
/// [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;
/// {@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;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// 将提供语义信息的子元素数量。
///
/// [ScrollView] 的一些子类型可以自动推断此值。例如 [ListView] 将使用子列表中的组件数量,
/// 而 [ListView.separated] 构造函数将使用该数量的一半。
///
/// 对于 [CustomScrollView] 和其他类型,它们不接收构建器或组件列表,必须明确提供子计数。如果数量未知或无限,则应保留未设置或设置为 null。
///
/// 另请参见:
///
/// * [SemanticsConfiguration.scrollChildCount],对应的语义属性。
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// 定义此 [ScrollView] 如何自动消除键盘的 [ScrollViewKeyboardDismissBehavior]。
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// 默认为 [Clip.hardEdge]。
final Clip clipBehavior;
/// 返回滚动视图滚动的 [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);
}
/// 构建放置在视口内的组件列表。
///
/// 子类应重写此方法,以构建视口内部的滑块。
///
/// 要了解更多关于滑块的信息,请参见 [CustomScrollView.slivers]。
@protected
List<Widget> buildSlivers(BuildContext context);
/// 构建视口(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
属性来进行构建。
@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 组件。
@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 的信息。
- 点赞
- 收藏
- 关注作者
评论(0)