本篇不是教您如何使用 API,而是一些日常开发中不常接触,但是很重要的内容。
Flutter 滑动列表
在 Flutter 里我们常见的滑动列表场景,简单地说其实是由三部分组成:
Viewport: 它是一个 MultiChildRenderObjectWidget 的控件,它提供的是一个 "视窗" 的作用,也就是列表所在的可视区域大小;
Scrollable: 它主要通过对手势的处理来实现滑动效果,比如 VerticalDragGestureRecognizer 和 HorizontalDragGestureRecognizer;
Sliver: 准确来说应该是 RenderSliver,它主要是用于在 Viewport 里面布局和渲染内容;
以 ListView 为例,如上图所示是 ListView 滑动过程的变化,其中:
绿色的 Viewport 就是我们看到的列表窗口大小;
紫色部分就是处理手势的 Scrollable,让黄色部分 SliverList 在 Viewport 里产生滑动;
黄色的部分就是 SliverList,当我们滑动时其实就是它在 Viewport 里的位置发生了变化;
准确说是完成 RenderSliver 的 performLayout 过程,通过 SliverConstraints 来得到对应的 SliverGeometry。
所以在 Flutter 里:
ListView 使用的是 SliverFixedExtentList 或者 SliverList;
GridView 使用的是 SliverGrid;
PageView 使用的是 SliverFillViewport;
当然这里有一个特殊的是 SingleChildScrollView,因为它是单个 child 的可滑动控件,它并没有使用 RenderSliver,而是直接自定义了一个 RenderObject (RenderBox),并且 在 performLayout 时直接调整 child 的 offset 来达到滑动效果。
RenderSliver
而事实上 RenderObejct 也可以分为两大基础子类:
RenderBox: 我们常用的布局控件都是基于 RenderBox 来实现布局;
RenderSliver: 主要用在 Viewport 里实现布局,Viewport 里的直属 children 也需要是 RenderSliver;
RenderBox
在 SingleChildScrollView 内部使用的是 RenderBox,那么在布局过程中自然而然会把整个 child 都进行布局和计算,绘制时主要也是通过 offset 和 clip 等来完成移动效果,这样的实现当 child 比较复杂或者过长时,性能就会变差。
RenderSliver
RenderSliver 的实现相对 RenderBox 就复杂更多,前面介绍过 RenderSliver 就是通过 SliverConstraints 来得到一个 SliverGeometry,其中:
简单地说就是可以实现 "懒加载",按需绘制,从而得到更流畅的滑动体验。
设定里每个 item 的高度为 114;
scrollExtent 是 2353,也就是整体可滑动距离等于 2353;
paintExtent 是 701,因为 ListView 的 Viewport 是 701,所以从 SliverConstraints 得到的 remainingPaintExtent 是 701,所以默认只需要绘制和布局高度为 701 的部分;(因为默认 paintExtent = layoutExtent)
对 item 多出的蓝色 8-9 部分,这是因为在 SliverConstraints 内会有一个叫 remainingCacheExtent 的参数,它表示了需要提前缓存的布局区域,也就是 "预布局" 的区域,这个区域默认大小是 defaultCacheExtent= 250.0;
ListView 高度为 701, defaultCacheExtent 为默认的 250,也就是得到 第一次需要布局到底部的距离其实为 951,按照每个 item 高度是 114,那么其实是有 8.3 个 item 高度,取整数也就是 9 个 item,最终得到整体需要处理的区域大小为 114 * 9 = 1026, 在 SliverList 内部就是 endScrollOffset 参数。
从这个例子可以看出,RenderSliver 在实现可滑动列表的开销和逻辑上,会比直接使用 RenderBox 好和灵活很多,同时也是为什么 Viewport 里需要使用 RenderSliver 而不是 RenderBox 的原因。
⚠️ 注意,这里比较容易有一个误区,那就是 ListView 是由 Viewport + Scrollable 和一个 RenderSliver 组成,所以在 ListView 里只会有一个 RenderSliver 而不是多个,想使用多个 RenderSliver 需要使用 CustomScrollView。
最后顺便聊下 CustomScrollView,事实上就是一个开放了可自定义配置 RenderSliver 数组的滑动控件,例如:
通过利用 SliverList + SliverGrid 就可以搭配出多样化的滑动列表;
通过 CupertinoSliverRefreshControl + SliverList 实现类似 iOS 原生的下拉刷新列表;
其他可用的内置 Sliver 还有: SliverPadding、SliverFillRemaining、SliverFillViewport、SliverPersistentHeader、SliverAppbar 等等。
NestedScrollView
如上图所示,NestedScrollView 内部主要是通过继承 CustomScrollView,然后自定义一个 NestedScrollViewViewport 来实现联动的效果。
那这有什么特别的呢?如下代码所示,这是使用 NestedScrollView 常用的模式,那有看出什么特别的地方了吗?
代码里 NestedScrollView 的 body 嵌套的是 ListView,前面我们介绍了 ListView 本身就是 Viewport + Scrollable + SliverList 组合,而 NestedScrollView 本身也有 NestedScrollViewViewport。
所以 NestedScrollView 的实现本质上其实就是 Viewport 嵌套 Viewport,会有两个 Scrollable 的存在,并且嵌套的 ListView 是被放在了 NestedScrollView 的 Sliver 里面,大致如下图所示。
这里面有几个关键的对象,其中:
SliverFillRemaining: 用于充满 Viewport 的剩余空间,在 NestedScrollView 里面就是充满 header 之外的剩余空间;
NestedScrollViewViewport: 在原 Viewport 的基础上增加了一个 SliverOverlapAbsorberHandle 参数,SliverOverlapAbsorberHandle 本身是一个 ChangeNotifier,主要是用来当 markNeedsLayout 时对外发出通知,比如对 header 部分;
_NestedScrollCoordinator 的实现比较复杂,简单地说 _NestedScrollCoordinator 内部创建了两个 _NestedScrollController:
_outerController: 属于 _NestedScrollViewCustomScrollView 的 controller,也就是它自己 controller;
_innerController: 属于 body 的 controller;
在 ListView 的父类 ScrollView 内部,默认情况下使用的就是 PrimaryScrollController.of(context) 这个 controller,因为 PrimaryScrollController 是一个 InheritedWidget。
而整个联动滑动的流程,主要就是 _NestedScrollCoordinator 里和它创建的两个 _NestedScrollController 有关系:
_NestedScrollController 的主要作用就是使用 _NestedScrollPosition 来替换 ScrollPosition;
_NestedScrollCoordinator 将 _outer 和 _inner 两个 _NestedScrollController 组合起来 (_outer 和 _inner 分别被应用到 NestedScrollView 和 body);
_NestedScrollPosition 内部将 Drag 等手势操作传递回 _NestedScrollCoordinator 里。
最后在 _NestedScrollCoordinator 的 drag 和 applyUserOffset 等方法里进行内外滚动的分配;
如上动图所示,可以看到 item0 并没有在橙色区域停止滑动,而是继续往上滑动,这就是因为作为 body 的列表不知道顶部有固定区域。
这时候就可以通过使用 SliverOverlapAbsorber + SliverOverlapInjector 的组合来解决这个问题:
在 SliverPersistentHeader 的外层嵌套一个 SliverOverlapAbsorber 用于吸收 SliverPersistentHeader 的高度;
使用 SliverOverlapInjector 将这个高度配置到 body 列表中,让列表知道顶部存在一个固定高度的区域;
https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/sliver_list_demo_page.dart
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"