# 6.1 可滚动组件简介

Flutter 中有两种布局模型:

  • 基于 RenderBox 的盒模型布局。
  • 基于 Sliver ( RenderSliver ) 按需加载列表布局。

前面章节的布局类型都是第一种,本节主要介绍一下第二种布局。

通常可滚动组件的子组件可能会非常多、占用的总高度也会非常大;如果要一次性将子组件全部构建出将会非常昂贵!为此,Flutter中提出一个Sliver(中文为“薄片”的意思)概念,Sliver 可以包含一个或多个子组件。Sliver 的主要作用是配合:加载子组件并确定每一个子组件的布局和绘制信息,如果 Sliver 可以包含多个子组件时,通常会实现按需加载模型。

只有当 Sliver 出现在视口中时才会去构建它,这种模型也称为“基于Sliver的列表按需加载模型”。可滚动组件中有很多都支持基于Sliver的按需加载模型,如ListViewGridView,但是也有不支持该模型的,如SingleChildScrollView

约定:后面如果我们说一个组件是Sliver 则表示它是基于Sliver布局的组件,同理,说一个组件是 RenderBox,则代表它是基于盒模型布局的组件,并不是说它就是 RenderBox 类的实例。

Flutter 中的可滚动主要由三个角色组成:Scrollable、Viewport 和 Sliver:

  • Scrollable :用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport 。
  • Viewport:显示的视窗,即列表的可视区域;
  • Sliver:视窗里显示的元素。

具体布局过程:

  1. Scrollable 监听到用户滑动行为后,根据最新的滑动偏移构建 Viewport 。
  2. Viewport 将当前视口信息和配置信息通过 SliverConstraints 传递给 Sliver。
  3. Sliver 中对子组件(RenderBox)按需进行构建和布局,然后确认自身的位置、绘制等信息,保存在 geometry 中(一个 SliverGeometry 类型的对象)。

比如有一个 ListView,大小撑满屏幕,假设它有 100 个列表项(都是RenderBox)且每个列表项高度相同,结构如下:

image-20210823113123105

图中白色区域为设备屏幕,也是 Scrollable 、 Viewport 和 Sliver 所占用的空间,三者所占用的空间重合,父子关系为:Sliver 父组件为 Viewport,Viewport的 父组件为 Scrollable 。注意ListView 中只有一个 Sliver,在 Sliver 中实现了子组件的按需加载。

其中顶部和底部灰色的区域为 cacheExtent,它表示预渲染的高度,需要注意这是在可视区域之外,如果 RenderBox 进入这个区域内,即使它还未显示在屏幕上,也是要先进行构建的,预渲染是为了后面进入 Viewport 的时候更丝滑。cacheExtent 的默认值是 250,在构建可滚动列表时我们可以指定这个值,这个值最终会传给 Viewport。

# Scrollable

用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport,我们看一下其关键的属性:

Scrollable({
  ...
  this.axisDirection = AxisDirection.down,
  this.controller,
  this.physics,
  required this.viewportBuilder, //后面介绍
})
1
2
3
4
5
6
7
  • axisDirection 滚动方向。
  • physics:此属性接受一个ScrollPhysics类型的对象,它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。默认情况下,Flutter会根据具体平台分别使用不同的ScrollPhysics对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在 iOS 上会出现弹性效果,而在 Android 上会出现微光效果。如果你想在所有平台下使用同一种效果,可以显式指定一个固定的ScrollPhysics,Flutter SDK中包含了两个ScrollPhysics的子类,他们可以直接使用:
    • ClampingScrollPhysics:列表滑动到边界时将不能继续滑动,通常在Android 中 配合 GlowingOverscrollIndicator(实现微光效果的组件) 使用。
    • BouncingScrollPhysics:iOS 下弹性效果。
  • controller:此属性接受一个ScrollController对象。ScrollController的主要作用是控制滚动位置和监听滚动事件。默认情况下,Widget树中会有一个默认的PrimaryScrollController,如果子树中的可滚动组件没有显式的指定controller,并且primary属性值为true时(默认就为true),可滚动组件会使用这个默认的PrimaryScrollController。这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能。我们将在本章后面“滚动控制”一节详细介绍ScrollController
  • viewportBuilder:构建 Viewport 的回调。当用户滑动时,Scrollable 会调用此回调构建新的 Viewport,同时传递一个 ViewportOffset 类型的 offset 参数,该参数描述 Viewport 应该显示那一部分内容。注意重新构建 Viewport 并不是一个昂贵的操作,应为 Viewport 本身也是 Widget,只是配置信息,Viewport 变化时对应的 RenderViewport 会更新信息,并不会随着 Widget 进行重新构建。

# 主轴和纵轴

在可滚动组件的坐标描述中,通常将滚动方向称为主轴,非滚动方向称为纵轴。由于可滚动组件的默认方向一般都是沿垂直方向,所以默认情况下主轴就是指垂直方向,水平方向同理。

# Viewport

Viewport 比较简单,用于渲染当前视口中需要显示 Sliver。

Viewport({
  Key? key,
  this.axisDirection = AxisDirection.down,
  this.crossAxisDirection,
  this.anchor = 0.0,
  required ViewportOffset offset, // 用户的滚动偏移
  // 类型为Key,表示从什么地方开始绘制,默认是第一个元素
  this.center,
  this.cacheExtent, // 预渲染区域
  //该参数用于配合解释cacheExtent的含义,也可以为主轴长度的乘数
  this.cacheExtentStyle = CacheExtentStyle.pixel, 
  this.clipBehavior = Clip.hardEdge,
  List<Widget> slivers = const <Widget>[], // 需要显示的 Sliver 列表
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

需要注意的是:

  • offset:该参数为Scrollabel 构建 Viewport 时传入,它描述了 Viewport 应该显示那一部分内容。
  • cacheExtent 和 cacheExtentStyle:CacheExtentStyle 是一个枚举,有 pixel 和 viewport 两个取值。当 cacheExtentStyle 值为 pixel 时,cacheExtent 的值为预渲染区域的具体像素长度;当值为 viewport 时,cacheExtent 的值是一个乘数,表示有几个 viewport 的长度,最终的预渲染区域的像素长度为:cacheExtent * viewport 的积, 这在每一个列表项都占满整个 Viewport 时比较实用,这时 cacheExtent 的值就表示前后各缓存几个页面。

# Sliver

Sliver 主要作用是对子组件进行构建和布局,比如 ListView 的 Sliver 需要实现子组件(列表项)按需加载功能,只有当列表项进入预渲染区域时才会去对它进行构建和布局、渲染。

Sliver 对应的渲染对象类型是 RenderSliver,RenderSliver 和 RenderBox 的相同点是都继承自 RenderObject 类,不同点是在布局的时候约束信息不同。RenderBox 在布局时父组件传递给它的约束信息对应的是 BoxConstraints,只包含最大宽高的约束;而 RenderSliver 在布局时父组件(列表)传递给它的约束是对应的是 SliverConstraints。关于 Sliver 的布局协议,我们将在本章最后一节中介绍。

# 可滚动组件的通用配置

几乎所有的可滚动组件在构造时都能指定 scrollDirection(滑动的主轴)、reverse(滑动方向是否反向)、controllerphysicscacheExtent ,这些属性最终会透传给对应的 Scrollable 和 Viewport,这些属性我们可以认为是可滚动组件的通用属性,后续再介绍具体的可滚动组件时将不再赘述。

reverse表示是否按照阅读方向相反的方向滑动,如:scrollDirection值为Axis.horizontal 时,即滑动发现为水平,如果阅读方向是从左到右(取决于语言环境,阿拉伯语就是从右到左)。reversetrue时,那么滑动方向就是从右往左。

# ScrollController

可滚动组件都有一个 controller 属性,通过该属性我们可以指定一个 ScrollController 来控制可滚动组件的滚动,比如可以通过ScrollController来同步多个组件的滑动联动。由于 ScrollController 是需要结合可滚动组件一起工作,所以本章中,我们会在介绍完 ListView 后详细介绍 ScrollController。

# 子节点缓存

按需加载子组件在大所数场景中都能有正收益,但是有些时候也会有副作用。比如有一个页面,它由一个ListView 组成,我们希望在页面顶部显示一块内容, 这部分内容的数据需要在每次页面打开时通过网络来获取,为此我们通一个 Header 组件来实现,它是一个 StatefulWidget ,会在initState 中请求网络数据,然后将它作为 ListView 的第一个孩子。现在问题来了,因为 ListView 是按需加载子节点的,这意味着如果 Header 滑出 Viewport 的预渲染区域之外时就会被销毁,重新滑入后又会被重新构建,这样就会发起多次网络请求,不符合我们期望。

为了解决上述问题,可滚动组件提供了一种缓存子节点的通用解决方案,它允许开发者对特定的子界限进行缓存,这个我们将在介绍后面小节中详细介绍。

# Scrollbar

Scrollbar是一个Material风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需将Scrollbar作为可滚动组件的任意一个父级组件即可,如:

Scrollbar(
  child: SingleChildScrollView(
    ...
  ),
);
1
2
3
4
5

ScrollbarCupertinoScrollbar都是通过监听滚动通知来确定滚动条位置的。关于的滚动通知的详细内容我们将在本章最后一节中专门介绍。

# CupertinoScrollbar

CupertinoScrollbar是 iOS 风格的滚动条,如果你使用的是Scrollbar,那么在iOS平台它会自动切换为CupertinoScrollbar

# 最后

本节我们介绍了 Flutter 中基于 Sliver 按需加载列表布局,后面的小节中我们会先介绍一下常用的可滚动组件(如ListViewGridViewCustomScrollView等),然后介绍一下ScrollController,最后我们通过一个实例介绍一下如何自定义 Sliver 。

请作者喝杯咖啡