ruby
/// 一个将 [Scrollable] 和 [Viewport] 结合起来,用于在一个维度上创建
/// 可交互的滚动内容区域的组件。
///
/// Scrollable 组件由三部分组成:
///
/// 1. 一个 [Scrollable] 组件,用来监听各种用户手势并实现滚动的交互逻辑。
/// 2. 一个视口组件,例如 [Viewport] 或 [ShrinkWrappingViewport],
/// 用来实现滚动的视觉效果,只显示滚动视图中的一部分子组件。
/// 3. 一个或多个 sliver,它们是可以组合的组件,用来创建不同的滚动效果,
/// 例如列表、网格、可展开的头部等。
///
/// [ScrollView] 通过创建 [Scrollable] 和视口,并将 sliver 的创建交给子类,
/// 来协调整个滚动结构。
///
/// 想了解更多关于 sliver 的内容,请参见 [CustomScrollView.slivers]。
///
/// 如果要控制滚动视图的初始滚动位置,可以提供一个 [controller],
/// 并在它的 [ScrollController.initialScrollOffset] 属性中设置初始偏移量。
///
/// {@template flutter.widgets.ScrollView.PageStorage}
/// ## 在会话期间保持滚动位置
///
/// 滚动视图会尝试使用 [PageStorage] 来保持它们的滚动位置。
/// 可以通过在 [controller] 上将 [ScrollController.keepScrollOffset] 设置为 false 来禁用该功能。
/// 如果启用,推荐为该组件的 [key] 使用 [PageStorageKey],
/// 以帮助区分不同的滚动视图,避免混淆。
/// {@endtemplate}
///
/// 另请参见:
///
/// * [ListView]:常用的 [ScrollView],显示一个线性滚动的子组件列表。
/// * [PageView]:显示一组子组件,每个子组件都与视口大小相同,可以滚动切换。
/// * [GridView]:一个 [ScrollView],显示一个二维滚动的子组件网格。
/// * [CustomScrollView]:一个 [ScrollView],通过使用 sliver 创建自定义滚动效果。
/// * [ScrollNotification] 和 [NotificationListener]:
/// 可以在不使用 [ScrollController] 的情况下监听滚动位置。
/// * [TwoDimensionalScrollView]:
/// 类似于 [ScrollView],但可以在二维方向上滚动。
ScrollView
kotlin
abstract class ScrollView extends StatelessWidget {
/// 创建一个可滚动的组件。
///
/// 如果没有提供 [controller],对于垂直方向的滚动视图,
/// [ScrollView.primary] 参数默认值为 true。
///
/// 当 [primary] 被显式设置为 true 时,必须保证 [controller] 为 null。
/// 如果 [primary] 为 true,那么这个滚动视图会自动绑定到
/// 最近的 [PrimaryScrollController]。
///
/// 如果 [shrinkWrap] 参数为 true,则 [center] 参数必须为 null。
///
/// [anchor] 参数的取值范围必须在 0 到 1 之间(包含 0 和 1)。
const ScrollView({
super.key,
this.scrollDirection = Axis.vertical, // 滚动方向,默认为垂直
this.reverse = false, // 是否反向滚动
this.controller, // 滚动控制器
this.primary, // 是否使用 PrimaryScrollController
ScrollPhysics? physics, // 滚动物理效果
this.scrollBehavior, // 滚动行为(如拖拽、滚轮等)
this.shrinkWrap = false, // 是否根据子组件大小收缩自身
this.center, // 定义滚动的中心 sliver
this.anchor = 0.0, // 视口锚点,范围 [0, 1]
this.cacheExtent, // 预渲染区域大小
this.semanticChildCount, // 语义子组件数量(无障碍支持)
this.dragStartBehavior = DragStartBehavior.start, // 拖拽行为的起始方式
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, // 键盘关闭行为
this.restorationId, // 状态恢复 ID
this.clipBehavior = Clip.hardEdge, // 裁剪行为
this.hitTestBehavior = HitTestBehavior.opaque, // 命中测试行为
}) : assert(
!(controller != null && (primary ?? false)),
// 如果 primary = true,则不能再手动传入 controller
'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),
// shrinkWrap = true 时,center 必须为 null
assert(anchor >= 0.0 && anchor <= 1.0),
// anchor 必须在 [0, 1] 之间
assert(semanticChildCount == null || semanticChildCount >= 0),
// semanticChildCount 不能为负数
physics =
physics ??
((primary ?? false) ||
(primary == null &&
controller == null &&
identical(scrollDirection, Axis.vertical))
? const AlwaysScrollableScrollPhysics()
: null);
// 如果没有指定 physics:
// - 当 primary = true
// - 或 primary = null 且 controller = null 且方向为垂直
// => 使用 AlwaysScrollableScrollPhysics(始终可滚动)
// 否则 physics = null
}
reverse
ruby
/// {@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;
controller
ruby
/// {@template flutter.widgets.scroll_view.controller}
/// 一个可以用来控制滚动视图滚动位置的对象。
///
/// 如果 [primary] 为 true,则必须为 null。
///
/// [ScrollController] 有多个用途:
/// - 可以用来控制滚动视图的初始滚动位置(参见 [ScrollController.initialScrollOffset])。
/// - 可以用来控制滚动视图是否自动保存并恢复滚动位置(参见 [ScrollController.keepScrollOffset])。
/// - 可以用来读取当前滚动位置(参见 [ScrollController.offset])或修改滚动位置(参见 [ScrollController.animateTo])。
/// {@endtemplate}
final ScrollController? controller;
cacheExtent
arduino
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
/// 缓存区域的扩展大小(以像素为单位)。
///
/// cacheExtent 决定了滚动视口外预渲染内容的范围,
/// 以提高滚动时的平滑性。值越大,滚动时可见区域之外的内容
/// 会被提前渲染,从而减少滚动时的卡顿,但同时会增加内存使用。
final double? cacheExtent;
-
cacheExtent
指视口之外预加载/预渲染的像素范围。 -
可以用来优化滚动性能,但数值过大可能占用过多内存。
buildSlivers
less
/// 构建放置在视口内的组件列表。
///
/// 子类应重写此方法,以构建视口内部的 sliver 列表。
///
/// 想了解更多关于 sliver 的内容,请参见 [CustomScrollView.slivers]。
@protected
List<Widget> buildSlivers(BuildContext context);
-
buildSlivers
是一个 受保护的方法 (@protected
),通常由ScrollView
的子类实现。 -
方法的返回值是 一个 Widget 列表 ,这些 Widget 必须是 sliver 类型 (如
SliverList
、SliverGrid
等),用于填充滚动视口。 -
这是
ScrollView
能够灵活支持各种滚动效果(列表、网格、头部折叠等)的核心接口。
buildViewport
php
/// 构建视口(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: '用来确定滚动视图的横轴方向',
hint:
'垂直滚动视图创建的 Viewport 会尝试从环境中的 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,
);
}
build
java
Widget build(BuildContext context) {
// 构建视口内部的 sliver 列表
final List<Widget> slivers = buildSlivers(context);
// 获取滚动方向(AxisDirection),结合 scrollDirection 和 reverse
final AxisDirection axisDirection = getDirection(context);
// 判断是否是 primary 滚动视图
final bool effectivePrimary =
primary ??
controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection);
// 根据是否 primary 决定使用哪个 ScrollController
final ScrollController? scrollController =
effectivePrimary ? PrimaryScrollController.maybeOf(context) : controller;
// 创建 Scrollable 组件
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior, // 拖拽起点行为
axisDirection: axisDirection, // 滚动方向
controller: scrollController, // 滚动控制器
physics: physics, // 滚动物理效果
scrollBehavior: scrollBehavior, // 滚动行为
semanticChildCount: semanticChildCount, // 语义子组件数量(无障碍)
restorationId: restorationId, // 状态恢复 ID
hitTestBehavior: hitTestBehavior, // 命中测试行为
viewportBuilder: (BuildContext context, ViewportOffset offset) {
// 使用 buildViewport 构建视口
return buildViewport(context, offset, axisDirection, slivers);
},
clipBehavior: clipBehavior, // 裁剪行为
);
// 如果是 primary 且有 scrollController,则阻止子孙 ScrollView 继承同一个 PrimaryScrollController
final Widget scrollableResult =
effectivePrimary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
// 根据 keyboardDismissBehavior 判断是否在拖拽时隐藏键盘
if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
return NotificationListener<ScrollUpdateNotification>(
child: scrollableResult,
onNotification: (ScrollUpdateNotification notification) {
final FocusScopeNode currentScope = FocusScope.of(context);
// 当拖拽发生且当前有焦点但不是 primaryFocus 时,收起键盘
if (notification.dragDetails != null &&
!currentScope.hasPrimaryFocus &&
currentScope.hasFocus) {
FocusManager.instance.primaryFocus?.unfocus();
}
return false; // 不阻止通知继续向上传递
},
);
} else {
return scrollableResult;
}
}