开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 25 天,点击查看活动详情
Sliver布局模型
Flutter有两种布局模型
- 基于RenderBox的盒模型布局
- 基于Sliver(RenderSliver)按需加载列表布局
通常可滚动组件的子组件可能会非常多,占用的总高度也会非常大;一次性加载对内存和性能的消耗会非常昂贵。FLutter中提出Sliver概念,Sliver可以包含多个子组件。Sliver的主要作用是配合:加载子组件并确定每一个子组件的布局和绘制信息,如果Sliver可以包含多个子组件时,通常会实现按需加载模型。
只有当Sliver出现在可视窗口时才会去构建它,这种模型也成为“基于Sliver的列表按需加载模型”。可滚动组件中有很多都是基于Sliver的按需加载模型,比如:ListView、GridView,也有不支持该模型的,比如: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类型的对象)。
举例:
图中白色为设备屏幕,也是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, //后面介绍
})
- axisDirection: 滚动方向
- controller:控制器:接收一个ScrollController对象,主要作用是控制滚动位置和监听滚动事件
- physics:此属性接收一个ScrollPhysics类型的对象,决定可滚动组件如何响应用户操作,比如用户滑动完,抬起手指后,继续执行动画;或滑动到边界时,如何显示。默认情况下,Flutter会根据不同平台分别使用不同的ScrollPhysics对象,应用不同的显示效果,如iOS滑动到边界继续拖动的话,会出现弹性效果,而安卓则会出现微光效果。如果想所有平台都使用同一种效果,可以指定一个固定的Scrollphysics,Flutter中包含两个ScrollPhysics的子类:1、ClampingScrollPhysics:列表滑动到边界将不能继续滑动,通常在安卓中配合GlowingOverscrollIndicator实现微光效果。2、BouncingScrollPhysics:iOS下弹性效果
- 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 列表
})
- offset: 该参数为Scrollable构建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。
可滚动组件的通用配置
几乎所有的可滚动组件在构造时都能指定scrollDirection(滑动的主轴)、reverse(滑动方向是否相反)、contriller、physics、cacheExtent,这些属性最终会透传给对应的Scrollable和Viewport,这些属性都可以认为是可滚动组件的通用属性。
reverse表示是否按照阅读方向相反的方向滑动,如:scrollDirection值为Axis.horizontal时,即滑动方向为水平,如果阅读方向时从左到右(取决于语言环境,阿拉伯语就是从右到左)。reverse为true时,那么滑动方向就是从右到左。
ScrollController
可滚动组件都有一个 controller 属性,通过该属性我们可以指定一个 ScrollController 来控制可滚动组件的滚动,比如可以通过ScrollController来同步多个组件的滑动联动
子节点缓存
按需加载子组件在大多数场景中都能有正收益,但是有些时候也会有副作用。比如有一个页面,它由一个ListView 组成,我们希望在页面顶部显示一块内容, 这部分内容的数据需要在每次页面打开时通过网络来获取,为此我们通一个 Header 组件来实现,它是一个 StatefulWidget ,会在initState
中请求网络数据,然后将它作为 ListView 的第一个孩子。现在问题来了,因为 ListView 是按需加载子节点的,这意味着如果 Header 滑出 Viewport 的预渲染区域之外时就会被销毁,重新滑入后又会被重新构建,这样就会发起多次网络请求,不符合我们期望,我们预期是Header能够缓存不销毁。
综上,为了方便控制子组件在滑出可视区域后是否缓存,可滚动组件提供了一种缓存子节点的通用解决方案,它允许开发者对特定的子界限进行缓存。
Scrollbar
Scrollbar是一个Material风格的滚动指示器(滚动条),如果要给滚动组件添加滚动条,只需将Scrollbar作为可滚动组件的任意一个父级组件即可,如:
Scrollbar(
child: SingleChildScrollView(
...
),
);
Scrollbar和CupertinoScrollbar都是通过监听滚动通知来确定滚动条位置的。
CupertinoScrollbar
CupertinoScrollbar是iOS风格的滚动条,如果使用的是Scollbar,那么在iOS平台会自动切换为CupertinoScrollbar。