0
点赞
收藏
分享

微信扫一扫

不一样角度带你了解 Flutter 中的滑动列表实现

本篇主要帮助剖析理解 Flutter 里的列表和滑动的组成,用比较通俗易懂的方式,从常见的 ​​ListView​​​ 到 ​​NestedScrollView​​ 的内部实现,帮助你更好理解和运用 Flutter 里的滑动列表。


本篇不是教你如何使用 API ,而是一些日常开发中不常接触,但是很重要的内容


Flutter 滑动列表

在 Flutter 里我们常见的滑动列表场景,简单地说其实是由三部分组成:

  • Viewport​ : 它是一个MultiChildRenderObjectWidget的控件 ,它提供的是一个“视窗”的作用,也就是列表所在的可视区域大小;
  • Scrollable​ :它主要通过对手势的处理来实现滑动效果,比如VerticalDragGestureRecognizerHorizontalDragGestureRecognizer;
  • Sliver​ : 准确来说应该是RenderSliver它主要是用于在 Viewport 里面布局和渲染内容;

不一样角度带你了解 Flutter 中的滑动列表实现_flutter

以 ​​ListView​​​ 为例,如上图所示是 ​​ListView​​ 滑动过程的变化,其中:

  • 绿色的​​Viewport​​ 就是我们看到的列表窗口大小;
  • 紫色部分就是处理手势的​​Scrollable​​​,让黄色部分​​SliverList​​​ 在​​Viewport​​ 里产生滑动;
  • 黄色的部分就是​​SliverList​​​ , 当我们滑动时其实就是它在​​Viewport​​ 里的位置发生了变化;

了解完这个基础理念后,就可以知道一般情况下 ​​Viewport​​​ 和 ​​Scrollable​​ 的实现都是很通用的,所以一般在 Flutter 里要实现不同的滑动列表,就是通过自定义和组合不同的 Sliver 来完成布局


准确说是完成 RenderSliver​performLayout​ 过程,通过 ​SliverConstraints​ 来得到对应的 ​SliverGeometry​


所以在 Flutter 里:

  • ​ListView​​​ 使用的是​​SliverFixedExtentList​​​ 或者​​SliverList​​;
  • ​GridView​​​ 使用的是​​SliverGrid​​;
  • ​PageView​​​ 使用的是​​SliverFillViewport​​;


当然这里有一个特殊的是 ​​SingleChildScrollView​​​ , 因为它是单个 ​​child​​​ 的可滑动控件,它并没有使用 ​​RenderSliver​​​,而是直接自定义了一个 ​​RenderObject​​(RenderBox) ,并且performLayout 时直接调整 ​child​​offset​ 来达到滑动效果


RenderSliver

我们都知道 Flutter 中的整体渲染流程是 Widget -> Element -> RenderObejct -> Layer 这样的过程,而 Flutter 里的布局和绘制逻辑都在 RenderObejct​。

而事实上 ​​RenderObejct​​ 也可以分为两大基础子类:

  • ​RenderBox​​ : 我们常用的布局控件都是基于 RenderBox来实现布局;
  • ​RenderSliver​​ :主要用在 Viewport 里实现布局Viewport里的直属children也需要是RenderSliver

那到这里你可能会有一个疑问:既然前面 ​​SingleChildScrollView​​​ 里没有使用 ​​RenderSliver​​​ ,直接使用 ​​RenderBox​​ 也可以实现滑动,为什么还要用 Viewport + RenderSliver 的方式来实现列表滑动?

RenderBox

在 ​​SingleChildScrollView​​​ 内部使用的是 ​​RenderBox​​​ ,那么在布局过程中自然而然会把整个 ​​child​​​ 都进行布局和计算,绘制时主要也是通过 ​​offset​​​ 和 ​​clip​​​ 等来完成移动效果,这样的实现当 ​child 比较复杂或者过长时,性能就会变差

RenderSliver

​RenderSliver​​​ 的实现相对 ​​RenderBox​​​ 就复杂更多,前面介绍过 ​RenderSliver 就是通过 ​SliverConstraints​ 来得到一个 ​SliverGeometry​,其中:

  • ​SliverConstraints​​ 中有 remainingPaintExtent 可以用来表示剩余的可绘制具体的大小;
  • ​SliverGeometry​​ 里也有 ​​scrollExtent​​ (可滑动的距离)、​​paintExtent​​(可绘制大小)、​​layoutExtent​​ (布局大小范围)、​​visible​​(是否需要绘制)等参数;

所以通过这部分参数,Viewport 里可以实现动态管理,节省资源,根据 ​SliverGeometry​ 判断需要绘制多大区域的内容,还剩多少内容可以绘制,需要加载的布局是哪些等等。

简单地说就是可以实现“懒加载”,按需绘制,从而得到更流畅的滑动体验。

不一样角度带你了解 Flutter 中的滑动列表实现_自定义_02

以 ​​ListView​​​ 为例,如上图所示是一个高为 701 的 ​​ListView​​​ ,实际布局渲染之后,对于 ​​SliverList​​​ 输出的 ​​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​ 参数


所以根据以上情况,​ListView 会输出一个 ​paintExtent​ 为 701 ,​cacheExtent​ 为 1026 的 ​SliverGeometry​

从这个例子可以看出,​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​​​ 单独拿出来说呢?这是因为 ​​NestedScrollView​​ 和前面介绍的滑动列表实现不大一样。

内部组成

不一样角度带你了解 Flutter 中的滑动列表实现_嵌套_03

如上图所示,​​NestedScrollView​​​ 内部主要是通过继承 ​​CustomScrollView​​​ ,然后自定义一个 ​​NestedScrollViewViewport​​ 来实现联动的效果。

那这有什么特别的呢?如下代码所示,这是使用 ​​NestedScrollView​​ 常用的模式,那有看出什么特别的地方了吗?

不一样角度带你了解 Flutter 中的滑动列表实现_flutter_04

代码里 ​​NestedScrollView​​​ 的 ​​body​​​ 嵌套的是 ​​ListView​​​ , 前面我们介绍了 ​​ListView​​​ 本身就是 ​​Viewport​​​ + ​​Scrollable​​​ + ​​SliverList​​​ 组合,而 ​​NestedScrollView​​​ 本身也有 ​​NestedScrollViewViewport​​。

所以 NestedScrollView 的实现本质上其实就是 ​Viewport​ 嵌套 ​Viewport​,会有两个 ​Scrollable​ 的存在 ,并且嵌套的 ​​ListView​​​ 是被放在了 ​​NestedScrollView​​​ 的 ​​Sliver​​ 里面,大致如下图所示。

不一样角度带你了解 Flutter 中的滑动列表实现_嵌套_05

这里面有几个关键的对象,其中:

  • ​SliverFillRemaining​​ :用于充满 ​​Viewport​​ 的剩余空间,在 ​​NestedScrollView​​ 里面就是充满 ​​header​​ 之外的剩余空间;
  • ​NestedScrollViewViewport​​ : 在原 ​​Viewport​​ 的基础上增加了一个 ​​SliverOverlapAbsorberHandle​​ 参数,​​SliverOverlapAbsorberHandle​​ 本身是一个 ​​ChangeNotifier​​ , 主要是用来当 ​​markNeedsLayout​​ 时对外发出通知,比如对 header 部分;

所以 ​​NestedScrollView​​​ 本质上两个 ​​Viewport​​ 之间的嵌套,那他们之间是滑动关系是如何处理的?这就要说到 NestedScrollView 里的 ​_NestedScrollCoordinator​ 对象。

_NestedScrollCoordinator

​_NestedScrollCoordinator​​​ 的实现比较复杂,简单地说 ​​_NestedScrollCoordinator​​​ 内部创建了两个 ​​_NestedScrollController​​:

  • ​_outerController​​​ :属于​​_NestedScrollViewCustomScrollView​​ 的controller,也就是它自己controller
  • ​_innerController​​​ :属于​​body​​ 的controller

不一样角度带你了解 Flutter 中的滑动列表实现_自定义_06


在 ​​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​​ 等方法里进行内外滚动的分配;

不一样角度带你了解 Flutter 中的滑动列表实现_嵌套_07

SliverPersistentHeader

了解完 ​​NestedScrollView​​​ 的布局和联动实现之外,最后简单介绍一下 ​​SliverPersistentHeader​​​ , 因为经常在 ​​NestedScrollView​​​ 里使用的 ​​SliverAppBar​​​,本质上 ​SliverAppBar 的实现靠的就是 ​SliverPersistentHeader​

​SliverPersistentHeader​​​ 主要是具备 ​​floating​​​ 和 ​​pinned​​​ 两个属性,它们的区别主要在于使用了不同的 ​​RenderSliver​​ 实现,而最终不同的地方其实就是输出 SliverGeometry 的不同

不一样角度带你了解 Flutter 中的滑动列表实现_前端_08

以第一个 ​​_SliverFloatingPinnedPersistentHeader​​​ 和最后一个 ​​_SliverScrollingPersistentHeader​​​ 之间的对比为例子,如下代码所示,在需要 ​​floating​​​ 和 ​​pinned​​​ 的 ​​Sliver​​​ 上,可以看到 ​​paintExtent​​​ 和 ​​layoutExtent​​ 都有一个最小值。

不一样角度带你了解 Flutter 中的滑动列表实现_前端_09

所以 Sliver 被固定住的原理,其实就是 ​Viewport​ 得到了它的 ​paintExtent​​layoutExtent​ 并不为 0,所以会继续为这个 ​Sliver​ 绘制对应区域的内容。

最后需要注意的是,当你使用 SliverPersistentHeader 去固定住头部的时候,作为 ​body​ 的列表是不知道顶部有个固定区域。 所以如果这时候不额外做一些处理,那么对于 ​​body​​​ 而言,它的 ​​paintOrigin​​ 还是从最顶部开始而不是固定区域的下方。

不一样角度带你了解 Flutter 中的滑动列表实现_flutter_10


如上动图所示,可以看到 item0 并没有在橙色区域停止滑动,而是继续往上滑动,这就是因为作为 ​​body​​ 的列表不知道顶部有固定区域。


这时候就可以通过使用 ​​SliverOverlapAbsorber​​​ + ​​SliverOverlapInjector​​ 的组合来解决这个问题:

  • 在 ​​SliverPersistentHeader​​ 的外层嵌套一个 ​​SliverOverlapAbsorber​​ 用于吸收 ​​SliverPersistentHeader​​ 的高度;
  • 使用 ​​SliverOverlapInjector​​ 将这个高度配置到 ​​body​​ 列表中,让列表知道顶部存在一个固定高度的区域;

不一样角度带你了解 Flutter 中的滑动列表实现_前端_11

这部分例子可见:​​github.com/CarGuo/gsy_…​​

好了,本篇关于 Flutter 滑动列表的实现原理就介绍完了,如果你还有什么想说的,欢迎留言讨论。

举报

相关推荐

0 条评论