不一样角度带你了解 Flutter 中的滑动列表实现,android开发从入门到精通第2版
以 ListView 为例,如上图所示是一个高为 701 的 ListView ,实际布局渲染之后,对于 SliverList 输出的 SliverGeom etry 而言:设定里每个 item 的高度为 114;scrollExtent 是 2353,也就是整体可滑动距离等于 2353;pain
以 ListView 为例,如上图所示是一个高为 701 的 ListView ,实际布局渲染之后,对于 SliverList 输出的 SliverGeom etry 而言:
- 设定里每个 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 和前面介绍的滑动列表实现不大一样。
内部组成

如上图所示,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 部分;
所以 NestedScrollView 本质上两个 Viewport 之间的嵌套,那他们之间是滑动关系是如何处理的?这就要说到 NestedScrollView 里的 _NestedScrollCoordinator 对象。
_NestedScrollCoordinator
_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等方法里进行内外滚动的分配;

SliverPersistentHeader
了解完 NestedScrollView 的布局和联动实现之外,最后简单介绍一下 SliverPersistentHeader , 因为经常在 NestedScrollView 里使用的 SliverAppBar,本质上 SliverAppBar 的实现靠的就是 SliverPersistentHeader。
SliverPersistentHeader 主要是具备 floating 和 pinned 两个属性,它们的区别主要在于使用了不同的 RenderSliver 实现,而最终不同的地方其实就是输出 SliverGeometry 的不同。

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

所以 Sliver 被固定住的原理,其实就是 Viewport 得到了它的 paintExtent 和 layoutExtent 并不为 0,所以会继续为这个 Sliver 绘制对应区域的内容。
最后需要注意的是,当你使用 SliverPersistentHeader 去固定住头部的时候,作为 body 的列表是不知道顶部有个固定区域。 所以如果这时候不额外做一些处理,那么对于 body 而言,它的 paintOrigin 还是从最顶部开始而不是固定区域的下方。
如上动图所示,可以看到 item0 并没有在橙色区域停止滑动,而是继续往上滑动,这就是因为作为
body的列表不知道顶部有固定区域。
这时候就可以通过使用SliverOverlapAbsorber+SliverOverlapInjector的组合来解决这个问题:
- 在
SliverPersistentHeader的外层嵌套一个SliverOverlapAbsorber用于吸收SliverPersistentHeader的高度; - 使用
SliverOverlapInjector将这个高度配置到body列表中,让列表知道顶部存在一个固定高度的区域;
这部分例子可见:github.com/CarGuo/gsy_…
好了,本篇关于 Flutter 滑动列表的实现原理就介绍完了,如果你还有什么想说的,欢迎留言讨论。
4318581)]
这部分例子可见:github.com/CarGuo/gsy_…
好了,本篇关于 Flutter 滑动列表的实现原理就介绍完了,如果你还有什么想说的,欢迎留言讨论。
更多推荐



所有评论(0)