不一样角度带您了解 Flutter 中的滑动列表实现 | 开发者说·DTalk

2022 年 2 月 18 日 谷歌开发者
本文原作者: 恋猫de小郭, 发布于: GSYTech


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


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



Flutter 滑动列表


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

  • Viewport: 它是一个 MultiChildRenderObjectWidget 的控件,它提供的是一个 "视窗" 的作用,也就是列表所在的可视区域大小;

  • Scrollable它主要通过对手势的处理来实现滑动效果,比如 VerticalDragGestureRecognizerHorizontalDragGestureRecognizer

  • Sliver: 准确来说应该是 RenderSliver它主要是用于在 Viewport 里面布局和渲染内容;


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

  • 绿色的 Viewport 就是我们看到的列表窗口大小;

  • 紫色部分就是处理手势的 Scrollable,让黄色部分 SliverListViewport 里产生滑动;

  • 黄色的部分就是 SliverList,当我们滑动时其实就是它在 Viewport 里的位置发生了变化;


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

准确说是完成 RenderSliverperformLayout 过程,通过 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 都进行布局和计算,绘制时主要也是通过 offsetclip 等来完成移动效果,这样的实现当 child 比较复杂或者过长时,性能就会变差


RenderSliver

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

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

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

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

ListView 为例,如上图所示是一个高为 701 的 ListView ,实际布局渲染之后,对于 SliverList 输出的 SliverGeometry 而言:
  • 设定里每个 item 的高度为 114;

  • scrollExtent 是 2353,也就是整体可滑动距离等于 2353;

  • paintExtent 是 701,因为 ListViewViewport 是 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 就可以搭配出多样化的滑动列表;

  • 通过 CupertinoSliverRefreshControlSliverList 实现类似 iOS 原生的下拉刷新列表;


其他可用的内置 Sliver 还有: SliverPaddingSliverFillRemainingSliverFillViewportSliverPersistentHeaderSliverAppbar 等等。



NestedScrollView


为什么会把 NestedScrollView 单独拿出来说呢?这是因为 NestedScrollView 和前面介绍的滑动列表实现不大一样。

内部组成

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


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

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


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

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

  • SliverFillRemaining: 用于充满 Viewport 的剩余空间,在 NestedScrollView 里面就是充满 header 之外的剩余空间;

  • NestedScrollViewViewport: 在原 Viewport 的基础上增加了一个 SliverOverlapAbsorberHandle 参数,SliverOverlapAbsorberHandle 本身是一个 ChangeNotifier,主要是用来当 markNeedsLayout 时对外发出通知,比如对 header 部分;


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

_NestedScrollCoordinator

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

  • _outerController: 属于 _NestedScrollViewCustomScrollViewcontroller,也就是它自己 controller

  • _innerController: 属于 bodycontroller

在 ListView 的父类 ScrollView 内部,默认情况下使用的就是 PrimaryScrollController.of(context) 这个 controller,因为 PrimaryScrollController 是一个 InheritedWidget


而整个联动滑动的流程,主要就是 _NestedScrollCoordinator 里和它创建的两个 _NestedScrollController 有关系:

  • _NestedScrollController 的主要作用就是使用 _NestedScrollPosition 来替换 ScrollPosition

  • _NestedScrollCoordinator 将 _outer 和 _inner 两个 _NestedScrollController 组合起来 (_outer 和 _inner 分别被应用到 NestedScrollViewbody);

  • _NestedScrollPosition 内部将 Drag 等手势操作传递回 _NestedScrollCoordinator 里。

  • 最后在 _NestedScrollCoordinatordragapplyUserOffset 等方法里进行内外滚动的分配;



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 列表中,让列表知道顶部存在一个固定高度的区域;


这部分 例子 可见:

https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/sliver_list_demo_page.dart


好了,本篇关于 Flutter 滑动列表的实现原理就介绍完了。



长按右侧二维码

查看更多开发者精彩分享




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。



 

 点击屏末 |  | 即刻报名参与 "开发者说·DTalk" 

 



登录查看更多
0

相关内容

国际概念建模会议(ER)是介绍和讨论当前概念建模研究的主要国际论坛。感兴趣的主题内容横跨整个概念建模包括等领域的研究和实践。 官网地址:http://dblp.uni-trier.de/db/conf/er/
【2021新书】面向对象的Python编程,418页pdf
专知会员服务
70+阅读 · 2021年12月15日
专知会员服务
22+阅读 · 2021年9月20日
【Manning新书】C++并行实战,592页pdf,C++ Concurrency in Action
流畅的Python 中英文版 PDF 高清电子书
专知会员服务
80+阅读 · 2020年8月2日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
20+阅读 · 2019年11月7日
探索 Flutter 模拟事件触发 | 开发者说·DTalk
谷歌开发者
5+阅读 · 2022年4月7日
我不认为Flutter比React Native好
InfoQ
0+阅读 · 2022年3月17日
我们为什么选择了Flutter Desktop | 开发者说·DTalk
谷歌开发者
0+阅读 · 2022年3月10日
Flutter 快速解析 TextField 的内部原理 | 开发者说·DTalk
如何理解 Flutter 路由源码设计?| 开发者说·DTalk
谷歌开发者
1+阅读 · 2022年1月28日
Flutter 如何与 Native (Android) 进行交互 | 开发者说·DTalk
Gradle 与 AGP 构建 API: 如何编写插件
谷歌开发者
0+阅读 · 2021年12月24日
Flutter 之美 | 开发者说·DTalk
谷歌开发者
1+阅读 · 2021年12月23日
国家自然科学基金
0+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
Arxiv
0+阅读 · 2022年4月19日
Salient Objects in Clutter
Arxiv
0+阅读 · 2022年4月18日
VIP会员
相关VIP内容
【2021新书】面向对象的Python编程,418页pdf
专知会员服务
70+阅读 · 2021年12月15日
专知会员服务
22+阅读 · 2021年9月20日
【Manning新书】C++并行实战,592页pdf,C++ Concurrency in Action
流畅的Python 中英文版 PDF 高清电子书
专知会员服务
80+阅读 · 2020年8月2日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
20+阅读 · 2019年11月7日
相关资讯
探索 Flutter 模拟事件触发 | 开发者说·DTalk
谷歌开发者
5+阅读 · 2022年4月7日
我不认为Flutter比React Native好
InfoQ
0+阅读 · 2022年3月17日
我们为什么选择了Flutter Desktop | 开发者说·DTalk
谷歌开发者
0+阅读 · 2022年3月10日
Flutter 快速解析 TextField 的内部原理 | 开发者说·DTalk
如何理解 Flutter 路由源码设计?| 开发者说·DTalk
谷歌开发者
1+阅读 · 2022年1月28日
Flutter 如何与 Native (Android) 进行交互 | 开发者说·DTalk
Gradle 与 AGP 构建 API: 如何编写插件
谷歌开发者
0+阅读 · 2021年12月24日
Flutter 之美 | 开发者说·DTalk
谷歌开发者
1+阅读 · 2021年12月23日
相关基金
国家自然科学基金
0+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
Top
微信扫码咨询专知VIP会员