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;
  }
}
相关推荐
Java 码农34 分钟前
nodejs koa留言板案例开发
前端·javascript·npm·node.js
ZhuAiQuan1 小时前
[electron]开发环境驱动识别失败
前端·javascript·electron
nyf_unknown1 小时前
(vue)将dify和ragflow页面嵌入到vue3项目
前端·javascript·vue.js
胡gh1 小时前
浏览器:我要用缓存!服务器:你缓存过期了!怎么把数据挽留住,这是个问题。
前端·面试·node.js
你挚爱的强哥2 小时前
SCSS上传图片占位区域样式
前端·css·scss
奶球不是球2 小时前
css新特性
前端·css
Nicholas682 小时前
flutter滚动视图之Viewport、RenderViewport源码解析(六)
前端
无羡仙2 小时前
React 状态更新:如何避免为嵌套数据写一长串 ...?
前端·react.js
TimelessHaze2 小时前
🔥 一文掌握 JavaScript 数组方法(2025 全面指南):分类解析 × 业务场景 × 易错点
前端·javascript·trae
jvxiao3 小时前
搭建个人博客系列--(4) 利用Github Actions自动构建博客
前端