flutter滚动视图之Scrollable源码解析(四)

ViewportBuilder

ini 复制代码
// 示例可以假设:
// late BuildContext context;

/// [Scrollable] 用来构建通过其显示可滚动内容的视口的签名。
typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset position);
  • Scrollable 是 Flutter 中用于处理滚动的控件。
  • 这个签名定义了如何通过传入的 BuildContextViewportOffset 来构建视口,该视口用于显示滚动内容。

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);
}

作用

ScrollableFlutter 所有可滚动组件的底层基础类,它本身不直接定义 UI 布局,而是提供:

  • 滚动手势处理(比如拖动、惯性滚动)。
  • 滚动位置管理 (通过 ScrollControllerScrollPosition)。
  • Viewport 的协作 (由 viewportBuilder 决定具体如何渲染子组件)。

但是,它不直接决定:

  • 子组件的布局方式(列表、网格、单子项)。
  • 具体的视口实现(ViewportShrinkWrappingViewport 等)。

所以大多数时候不会直接用 Scrollable,而是用它的上层封装:

  • ListViewGridView(内置列表和网格布局)。
  • 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;
  }
}

📌 解释

  1. 作用

    • _ScrollableScope 是一个 InheritedWidget ,包装了 ScrollableStateScrollPosition
    • 它的主要目的是让 Scrollable.of(context) / maybeOf(context) 可以通过 context.dependOnInheritedWidgetOfExactType 来找到最近的 ScrollableState
  2. 为什么要用 InheritedWidget

    • 通过 InheritedWidget,子 widget 可以 自动依赖滚动状态 ,当滚动位置变化时自动重建(通过 updateShouldNotify 判断)。
    • 保证了嵌套滚动场景中,子 widget 可以正确获取滚动信息。
  3. updateShouldNotify

    • position(滚动位置)变化时返回 true,通知依赖此 scope 的 widget 重新 build。
    • 这样可以保证需要知道滚动位置的子 widget 能够随滚动更新。
相关推荐
golang学习记10 分钟前
从0死磕全栈之Next.js App Router动态路由详解:从入门到实战
前端
huangql52012 分钟前
基于前端+Node.js 的 Markdown 笔记 PDF 导出系统完整实战
前端·笔记·node.js
在逃的吗喽27 分钟前
Vue3新变化
前端·javascript·vue.js
yqwang_cn30 分钟前
打造优雅的用户体验:自定义jQuery工具提示插件开发全解析
前端·jquery·ux
小Tomkk33 分钟前
AI 提效:利用 AI 从前端 快速转型为UI/UX设计师和产品
前端·人工智能·ui
Demoncode_y1 小时前
Vue3中基于路由的动态递归菜单组件实现
前端·javascript·vue.js·学习·递归·菜单组件
杨超越luckly1 小时前
HTML应用指南:利用POST请求获取全国中国工商农业银行网点位置信息
大数据·前端·html·数据可视化·银行网点
皮蛋瘦肉粥_1212 小时前
pink老师html5+css3day02
前端·css3·html5
qianmo20212 小时前
基于pycharm实现html文件的快速实现问题讨论
前端·html
IT_陈寒2 小时前
SpringBoot3踩坑实录:一个@Async注解让我多扛了5000QPS
前端·人工智能·后端