flutter滚动视图之ScrollView源码解析(五)

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 类型 (如 SliverListSliverGrid 等),用于填充滚动视口。

  • 这是 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;
  }
}
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax