Flutter 中有两种布局模型:基于 RenderBox 的盒模型布局;基于 Sliver ( RenderSliver ) 按需加载列表布局。
listview各个构造函数的共同参数
double? itemExtent,
Widget? prototypeItem, //列表项原型,后面解释
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double? cacheExtent, // 预渲染区域长度
注意,itemExtent 和prototypeItem 互斥,不能同时指定它们。(没找到prototypeItem)
ListView 内部组合了 Scrollable、Viewport 和 Sliver,需要注意:ListView 中的列表项组件都是 RenderBox,并不是 Sliver, 这个一定要注意。一个 ListView 中只有一个Sliver,对列表项进行按需加载的逻辑是 Sliver 中实现的。ListView 的 Sliver 默认是 SliverList,如果指定了 itemExtent ,则会使用 SliverFixedExtentList;如果 prototypeItem 属性不为空,则会使用SliverPrototypeExtentList,无论是是哪个,都实现了子组件的按需加载模型。
一个ScrollController对象可以同时被多个可滚动组件使用,ScrollController会为每一个可滚动组件创建一个ScrollPosition(ScrollPosition是用来保存可滚动组件的滚动位置的)对象,这些ScrollPosition保存在ScrollController的positions属性中(List
实践中,我们发现,pageView并没有缓存功能,一旦页面滑出屏幕,它就会被销毁,这和ListView/GridView不一样,在创建 ListView/GridView 时我们可以手动指定 ViewPort 之外多大范围内的组件需要预渲染和缓存(通过 cacheExtent 指定),只有当组件滑出屏幕后又滑出预渲染区域,组件才会被销毁,但是不幸的是PageView 并没有 cacheExtent 参数!于是便引出了AutomaticKeepAlive组件。
AutomaticKeepAlive 的组件的主要作用是将列表项的根 RenderObject 的 keepAlive 按需自动标记 为 true 或 false。为了方便叙述,我们可以认为根RenderObject 对应的组件就是列表项的根 Widget,代表整个列表项组件,同时我们将列表组件的 Viewport区域 + cacheExtent(预渲染区域)称为加载区域;
如果有则直接复用,如果没有则重新创建列表项。
于是,为了解决pageview的缓存问题,只需要让Page页变成一个AutomaticKeepAlive Client即可。为了便于开发者实现,Flutter提供了一个AutomaticKeepAliveClientMixin,我们只需要让 PageState 混入这个 mixin,且同时添加一些必要操作即可。
class _PageState extends State with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return Center(child: Text("${widget.text}", textScaleFactor: 5));
}
@override
bool get wantKeepAlive => true; // 是否需要缓存
}
我们只需要提供一个wantKeepAlive,它会表示AutomaticKeepAlive是否需要缓存当前列表项;另外我们必须在build方法中调用一下super.build(context),该方法实现在 AutomaticKeepAliveClientMixin 中,功能就是根据当前 wantKeepAlive 的值给 AutomaticKeepAlive 发送消息,AutomaticKeepAlive 收到消息后就会开始工作。
前面介绍的 ListView、GridView、PageView 都是一个完整的可滚动组件,所谓完整是指它们都包括Scrollable 、 Viewport 和 Sliver。每一个 ListView 只会响应自己可视区域中滑动,因为他们都有独立的Scrollable 、 Viewport 和 Sliver。CustomScrollView 的主要功能是提供一个公共的的 Scrollable 和 Viewport,来组合多个 Sliver。
例如:
Widget buildTwoSliverList() {
// SliverFixedExtentList 是一个 Sliver,它可以生成高度相同的列表项。
// 再次提醒,如果列表项高度相同,我们应该优先使用SliverFixedExtentList
// 和 SliverPrototypeExtentList,如果不同,使用 SliverList.
var listView = SliverFixedExtentList(
itemExtent: 50, //列表项高度固定
delegate: SliverChildBuilderDelegate(
(_, index) => ListTile(title: Text('$index')),
childCount: 10,
),
);
// 使用
return CustomScrollView(
slivers: [
listView,
listView,
],
);
}
由于CustomScrollView的子组件必须都是sliver,所以,对应的组件一般都有相应的sliver
例如:
Sliver名称 | 功能 | 对应的可滚动组件 |
---|---|---|
SliverList | 列表 | ListView |
SliverFixedExtentList | 高度固定的列表 | ListView,指定itemExtent时 |
SliverAnimatedList | 添加/删除列表项可以执行动画 | AnimatedList |
SliverGrid | 网格 | GridView |
SliverPrototypeExtentList | 根据原型生成高度固定的列表 | ListView,指定prototypeItem 时 |
SliverFillViewport | 包含多给子组件,每个都可以填满屏幕 | PageView |
除了和列表对应的 Sliver 之外还有一些用于对 Sliver 进行布局、装饰的组件,它们的子组件必须是 Sliver,我们列举几个常用的:
Sliver名称 | 对应 RenderBox |
---|---|
SliverPadding | Padding |
SliverVisibility、SliverOpacity | Visibility、Opacity |
SliverFadeTransition | FadeTransition |
SliverLayoutBuilder | LayoutBuilder |
还有一些其它常用的 Sliver:
Sliver名称 | 说明 |
---|---|
SliverAppBar | 对应 AppBar,主要是为了在 CustomScrollView 中使用。 |
SliverToBoxAdapter | 一个适配器,可以将 RenderBox 适配为 Sliver。 |
SliverPersistentHeader | 滑动到顶部时可以固定住。 |
CustomScrollView对比SingleChildScrollView
通常SingleChildScrollView只应在期望的内容不会超过屏幕太多时使用,这是因为SingleChildScrollView不支持基于 Sliver 的延迟加载模型,所以如果预计视图可能包含超出屏幕尺寸太多的内容时,那么使用SingleChildScrollView将会非常昂贵(性能差),此时应该使用一些支持Sliver延迟加载的可滚动组件,如ListView。
需要注意的是,如果 CustomScrollView 有孩子也是一个完整的可滚动组件且它的滑动方向和CustomScrollView一致,则 CustomScrollView 不能正常工作,要解决这个问题,可以使用 NestedScrollView。例如:CustomScrollView 是处理垂直方向的,子组件 ListView 也是垂直方向的时,则不能正常工作。最终的效果是,在ListView内滑动时只会对ListView 起作用,原因是滑动事件被 ListView 的 Scrollable 优先消费,CustomScrollView 的 Scrollable 便接收不到滑动事件了。(这个问题我没有复现)
Flutter 中手势的冲突时,默认的策略是子元素生效
不要直接使用listView嵌套listview,因为父组件无法判断子listView 的高度,可以通过设置shrinkWrap = true 来修正。但是这会引出flutter性能差的问题,因为每次,都要计算所有content的高度,这个时候可以考虑使用CustomScrollView,在sliver里面放置SliverList即可。
关于NestedScrollView和CustomScrollView的对比,CustomScrollView是基于Sliver(Sliver通常指具有特定滚动效果的可滚动块)来自定义滚动模型的ScrollView的模型,它可以包含多种滚动类型,包含listview、gridview,并且要求页面的滚动效果是统一的。
NestedScrollView实际上是CustomScrollView的一个子类,NestScrollView包含header和body,它的header和body都是CustomScrollView的子Sliver,注意,虽然body是一个RenderBox,但是它会被包装为Sliver 。
NestedScrollView 核心功能就是通过一个协调器来协调外部可滚动组件和内部可滚动组件的滚动,以使滑动效果连贯统一,协调器的实现原理就是分别给内外可滚动组件分别设置一个 controller,然后通过这两个controller 来协调控制它们的滚动。
需要注意的是在使用NestScrollView包裹CustomScrollView时,直接使用会出现滑动的时候SliverAppBar遮挡CustomScrollView的列表内容,可以使用
SliverOverlapAbsorber(它的作用就是获取 SliverAppBar 返回时遮住内部可滚动组件的部分的长度,这个长度就是 overlap(重叠) 的长度)将SliverAppBar包裹一下,同时,给CustomScrollview的列表包裹一个 SliverOverlapInjector(它会将 SliverOverlapAbsorber 中获取的overlap 长度应用到内部可滚动组件中)。SliverOverlapAbsorber和SliverOverlapInjector都需要传入一个handle,具体指:NestedScrollView.sliverOverlapAbsorberHandleFor(context)。这样便是标准的解决方案。