ViewportBuilder
ini
// 示例可以假设:
// late BuildContext context;
/// [Scrollable] 用来构建通过其显示可滚动内容的视口的签名。
typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset position);
Scrollable
是 Flutter 中用于处理滚动的控件。- 这个签名定义了如何通过传入的
BuildContext
和ViewportOffset
来构建视口,该视口用于显示滚动内容。
TwoDimensionalViewportBuilder
dart
/// 用于 [TwoDimensionalScrollable] 构建视口的函数签名,
/// 可滚动内容通过该视口进行显示。
typedef TwoDimensionalViewportBuilder = Widget Function(
BuildContext context,
ViewportOffset verticalPosition, // 垂直方向滚动位置控制
ViewportOffset horizontalPosition, // 水平方向滚动位置控制
);
_EnsureVisibleResults
arduino
// _performEnsureVisible 的返回类型。
//
// futures 列表表示每一个尚未完成的 ScrollPosition.ensureVisible 调用。
// 返回的 ScrollableState 的 context 被用来查找下一个可能的祖先 Scrollable。
typedef _EnsureVisibleResults = (List<Future<void>>, ScrollableState);
Scrollable
kotlin
/// 一个在单一维度上管理滚动并通知 [Viewport] 来展示内容的组件。
///
/// [Scrollable] 实现了滚动组件的交互模型,包括手势识别,
/// 但它并不关心真正展示子组件的视口(viewport)是如何构建的。
///
/// 很少会直接构建一个 [Scrollable]。通常你会使用 [ListView] 或 [GridView],
/// 它们结合了滚动、视口以及布局模型。如果你想组合不同的布局模型
/// (或使用自定义的布局模式),可以考虑使用 [CustomScrollView]。
///
/// [Scrollable.of] 和 [Scrollable.ensureVisible] 这两个静态函数通常用于
/// 在 [ListView] 或 [GridView] 内与 [Scrollable] 小部件交互。
///
/// 想要进一步自定义 [Scrollable] 的滚动行为:
///
/// 1. 你可以提供一个 [viewportBuilder] 来定制子组件的模型。
/// 例如,[SingleChildScrollView] 使用一个只显示单个 box 子组件的视口,
/// 而 [CustomScrollView] 使用 [Viewport] 或 [ShrinkWrappingViewport],
/// 它们都能展示一个 sliver 列表。
///
/// 2. 你可以提供一个自定义的 [ScrollController],并由它创建一个自定义的
/// [ScrollPosition] 子类。例如,[PageView] 使用 [PageController],
/// 它会创建一个面向"页面"的滚动位置子类,
/// 从而保证在 [Scrollable] 调整大小时,仍然保持同一页可见。
///
/// ## 在会话期间保持滚动位置
///
/// Scrollable 会尝试使用 [PageStorage] 来保存滚动位置。
/// 可以通过将 [controller] 的 [ScrollController.keepScrollOffset] 设置为 false 来禁用这一行为。
/// 如果启用,建议为此 widget(或其祖先,如 [ScrollView])提供一个 [PageStorageKey] 作为 [key],
/// 以帮助区分不同的 [Scrollable] 实例。
///
/// 参见:
///
/// * [ListView]:常用的 [ScrollView],显示一个线性滚动列表。
/// * [PageView]:显示一组与视口大小相同的子组件,可分页滚动。
/// * [GridView]:[ScrollView] 的一种,显示二维网格状的子组件。
/// * [CustomScrollView]:[ScrollView] 的一种,可以通过 slivers 实现自定义滚动效果。
/// * [SingleChildScrollView]:只有单个子组件的可滚动组件。
/// * [ScrollNotification] 和 [NotificationListener]:
/// 不依赖 [ScrollController] 也能监听滚动位置。
class Scrollable extends StatefulWidget {
/// Creates a widget that scrolls.
const Scrollable({
super.key,
this.axisDirection = AxisDirection.down,
this.controller,
this.physics,
required this.viewportBuilder,
this.incrementCalculator,
this.excludeFromSemantics = false,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.restorationId,
this.scrollBehavior,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
}) : assert(semanticChildCount == null || semanticChildCount >= 0);
}
作用
Scrollable
是 Flutter 所有可滚动组件的底层基础类,它本身不直接定义 UI 布局,而是提供:
- 滚动手势处理(比如拖动、惯性滚动)。
- 滚动位置管理 (通过
ScrollController
和ScrollPosition
)。 - 与
Viewport
的协作 (由viewportBuilder
决定具体如何渲染子组件)。
但是,它不直接决定:
- 子组件的布局方式(列表、网格、单子项)。
- 具体的视口实现(
Viewport
、ShrinkWrappingViewport
等)。
所以大多数时候不会直接用 Scrollable
,而是用它的上层封装:
ListView
、GridView
(内置列表和网格布局)。CustomScrollView
(灵活组合 sliver,做复杂的滚动效果)。SingleChildScrollView
(单个子组件可滚动)。
controller
ruby
/// {@template flutter.widgets.Scrollable.controller}
/// 一个可以用来控制此组件滚动位置的对象。
///
/// [ScrollController] 有多个用途:
/// - 可以用来控制初始滚动位置(参见 [ScrollController.initialScrollOffset])。
/// - 可以用来控制滚动视图是否应该在 [PageStorage] 中自动保存和恢复滚动位置
/// (参见 [ScrollController.keepScrollOffset])。
/// - 可以用来读取当前的滚动位置(参见 [ScrollController.offset]),
/// 或者修改它(参见 [ScrollController.animateTo])。
///
/// 如果为 null,[Scrollable] 会在内部创建一个 [ScrollController],
/// 以便创建和管理 [ScrollPosition]。
///
/// 参见:
/// * [Scrollable.ensureVisible]:它会通过动画滚动到指定的 [BuildContext],
/// 以确保目标 widget 可见。
/// {@endtemplate}
final ScrollController? controller;
viewportBuilder(重要)
arduino
/// 构建用于显示可滚动内容的视口(viewport)。
///
/// 一个典型的视口会使用提供的 [ViewportOffset] 来决定
/// 其内容的哪一部分实际处于可见范围之内。
///
/// 参见:
///
/// * [Viewport]:一种视口,用于显示一组 sliver。
/// * [ShrinkWrappingViewport]:一种视口,用于显示一组 sliver,
/// 并且会根据这些 sliver 的大小自动调整自身大小。
final ViewportBuilder viewportBuilder;
📌 解释
viewportBuilder
:就是一个函数,用来构建 视口 (Viewport) 。- Viewport 的职责:决定在滚动的某一时刻,哪些内容应该被绘制(可见),哪些在可见范围外则不绘制(提升性能)。
- ViewportOffset :代表滚动位置(比如滚动多少像素),
Viewport
会用它来判断"现在内容的哪一段要显示"。
👉 换句话说:
Scrollable
负责滚动交互viewportBuilder
决定"滚动时具体显示什么内容"
maybeOf
ruby
/// 返回包裹给定 [context] 的最近的该类实例对应的状态 [ScrollableState],
/// 如果找不到则返回 null。
///
/// 典型的用法如下:
///
/// ```dart
/// ScrollableState? scrollable = Scrollable.maybeOf(context);
/// ```
///
/// 调用此方法时,如果找到了 [ScrollableState],
/// 将会在返回的 [ScrollableState] 上创建依赖关系。
/// 通常这会是最近的 [Scrollable],
/// 但如果使用了 [axis] 参数来指定目标滚动方向,
/// 那么也可能是一个更远的祖先 [Scrollable]。
///
/// 当滚动视图(Scrollables)是嵌套的时,传入可选的 [Axis] 参数会很有用,
/// 因为目标 [Scrollable] 可能并不是离得最近的实例。
/// 如果提供了 [axis],则会返回该方向上最近的 [ScrollableState],
/// 如果没有则返回 null。
///
/// 注意,这个方法只会查找 `context` 的最近 **祖先** [Scrollable]。
/// 这意味着如果 `context` 本身就是一个 [Scrollable] 的话,
/// 它不会返回该 [Scrollable] 自己。
///
/// 另见:
///
/// * [Scrollable.of]:与此方法类似,但如果找不到任何 [Scrollable] 祖先会抛出异常。
_ScrollableScope
scala
// 使得 Scrollable.of() 能像使用 InheritedWidget 一样工作,
// 通过依赖 _ScrollableScope 来访问 ScrollableState。
// ScrollableState.build() 总是会重新构建它的 _ScrollableScope。
class _ScrollableScope extends InheritedWidget {
const _ScrollableScope({
required this.scrollable,
required this.position,
required super.child,
});
/// 当前的 ScrollableState
final ScrollableState scrollable;
/// 对应的滚动位置 ScrollPosition
final ScrollPosition position;
@override
bool updateShouldNotify(_ScrollableScope old) {
// 当滚动位置改变时,通知依赖此 InheritedWidget 的子 widget 重建
return position != old.position;
}
}
📌 解释
-
作用
_ScrollableScope
是一个 InheritedWidget ,包装了ScrollableState
和ScrollPosition
。- 它的主要目的是让
Scrollable.of(context)
/maybeOf(context)
可以通过context.dependOnInheritedWidgetOfExactType
来找到最近的ScrollableState
。
-
为什么要用 InheritedWidget
- 通过 InheritedWidget,子 widget 可以 自动依赖滚动状态 ,当滚动位置变化时自动重建(通过
updateShouldNotify
判断)。 - 保证了嵌套滚动场景中,子 widget 可以正确获取滚动信息。
- 通过 InheritedWidget,子 widget 可以 自动依赖滚动状态 ,当滚动位置变化时自动重建(通过
-
updateShouldNotify
- 当
position
(滚动位置)变化时返回 true,通知依赖此 scope 的 widget 重新 build。 - 这样可以保证需要知道滚动位置的子 widget 能够随滚动更新。
- 当