想象有一种应用场景,scrollable组件内部嵌套另一个scrollable组件。特别是当同时显示ListView和GridView时应该怎么处理。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | return Scaffold (appBar: AppBar(
 title: Text('listView')
 ),
 body: Container(
 child: SingleChildScrollView(
 child: Column(
 children: <Widget>[
 someWidget,
 someWidget,
 someWidget,
 ListView(
 children: <Widget>[
 anotherWidget,
 anotherWidget,
 anotherWidget
 ]
 )
 ]
 )
 )
 )
 )
 
 | 
运行以上代码,控制台会抛出异常,其中关键的一句翻译过来就是:
垂直视口被赋予无限高度。这种情况通常在可滚动小部件嵌套在另一个可滚动小部件内时发生。
这个时候,我们可以通过Slivers来实现这一需求。通过以下代码替换:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
 | return Scaffold (appBar: AppBar(
 title: Text('listView')
 ),
 body: Container(
 child: CustomScrollView(
 slivers: <Widget>[
 SliverList(
 delegate: SliverChildListDelegate(
 someWidget,
 someWidget,
 someWidget,
 )
 ),
 SliverList(
 delegate: SliverChildListDelegate(
 anotherWidget,
 anotherWidget,
 anotherWidget,
 )
 ),
 SliverGrid(
 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
 delegate: SliverChildListDelegate(
 thirdWidget,
 thirdWidget,
 thirdWidget,
 )
 ),
 ]
 )
 )
 )
 
 | 
下面介绍Slivers系列常见的控件及使用场景
SliverAppBar
经常用于AppBar展开收起的场景,通过配置flexibleSpace和expandedHeight属性
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | CustomScrollView(slivers: <Widget>[
 SliverAppBar(
 actions: <Widget>[
 _buildAction(),
 ],
 title: Text('SliverAppBar'),
 backgroundColor: Theme.of(context).accentColor,
 expandedHeight: 200.0,
 flexibleSpace: FlexibleSpaceBar(
 background: Image.asset('images/food01.jpeg', fit: BoxFit.cover),
 ),
 ),
 SliverFixedExtentList(
 itemExtent: 120.0,
 delegate: SliverChildListDelegate(
 products.map((product) {
 return _buildItem(product);
 }).toList(),
 ),
 ),
 ],
 );
 
 | 
flexibleSpace是被展开和收起的组件,expandedHeight是其操控的高度;其他属性具体含义可以参考官方文档
SliverList
SliverList只需要设置delegate属性就可以,可以滑动的列表,常常用于滑动组件嵌套的场景
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | CustomScrollView(slivers: <Widget>[
 SliverList(
 delegate: SliverChildBuilderDelegate(
 (BuildContext context, int index) {
 return _buildItem(context, products[index]);
 },
 childCount: 3,
 ),
 )
 ],
 );
 
 | 
也可以通过SliverChildListDelegate来构建
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | CustomScrollView(slivers: <Widget>[
 SliverList(
 delegate: SliverChildListDelegate([
 _buildItem(),
 _buildItem(),
 _buildItem(),
 ]),
 )
 ],
 );
 
 | 
SliverChildListDelegate一般用来构item建数量明确的列表,会提前build好所有的子item,所以在效率上会有问题,适合item数量不多的情况。
SliverChildBuilderDelegate构建的列表理论上是可以无限长的。
两者的区别有些类似于ListView和ListView.builder()的区别。
SliverGrid
SliverGrid有三个构造函数:SliverGrid.count()、SliverGrid.extent和SliverGrid()。
- SliverGrid.count()指定了一行展示多少个item,下面的例子表示一行展示4个:
| 1
 | SliverGrid.count(children: scrollItems, crossAxisCount: 4)
 | 
- SliverGrid.extent可以指定item的最大宽度,然后让Flutter自己决定一行展示多少个item
| 1
 | SliverGrid.extent(children: scrollItems, maxCrossAxisExtent: 90.0)
 | 
- SliverGrid()则是需要指定一个gridDelegate,它提供给了程序员一个自定义Delegate的入口,你可以自己决定每一个item怎么排列
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | SliverGrid(gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
 crossAxisCount: 2
 ),
 delegate: SliverChildBuilderDelegate(
 (BuildContext context, int index) {
 return _buildItem(products[index]);;
 }
 );
 
 | 
SliverPersistentHeader顾名思义,就是给一个可滑动的视图添加一个头(实际上,在CustomScrollView的slivers列表中,header可以出现在视图的任意位置,不一定要是在顶部)。这个Header会随着滑动而展开/收起
| 12
 3
 4
 5
 6
 7
 
 | SliverPersistentHeader(delegate: _SliverAppBarDelegate(
 minHeight: 60.0,
 maxHeight: 180.0,
 child: Container(),
 ),
 );
 
 | 
构建一个SliverPersistentHeader需要传入一个delegate,这个delegate是SliverPersistentHeaderDelegate类型的,而SliverPersistentHeaderDelegate是一个abstract类,我们不能直接new一个SliverPersistentHeaderDelegate出来,因此,我们需要自定义一个delegate来实现SliverPersistentHeaderDelegate类:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 
 | class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {_SliverAppBarDelegate({
 @required this.minHeight,
 @required this.maxHeight,
 @required this.child,
 });
 
 final double minHeight;
 final double maxHeight;
 final Widget child;
 
 @override
 double get minExtent => minHeight;
 
 @override
 double get maxExtent => math.max(maxHeight, minHeight);
 
 @override
 Widget build(
 BuildContext context, double shrinkOffset, bool overlapsContent) {
 return new SizedBox.expand(child: child);
 }
 
 @override
 bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
 return maxHeight != oldDelegate.maxHeight ||
 minHeight != oldDelegate.minHeight ||
 child != oldDelegate.child;
 }
 }
 
 | 
写一个自定义SliverPersistentHeaderDelegate很简单,只需重写build()、get maxExtent、get minExtent和shouldRebuild()这四个方法,上面就是一个最简单的SliverPersistentHeaderDelegate的实现。其中,maxExtent表示header完全展开时的高度,minExtent表示header在收起时的最小高度。因此,对于我们上面的那个自定义Delegate,如果将minHeight和maxHeight的值设置为相同时,header就不会收缩了,这样的Header跟我们平常理解的Header更像。
SliverToBoxAdapter
 SliverPersistentHeader一般来说都是会展开/收起的(除非minExtent和maxExtent值相同),那么如果想要在滚动视图中添加一个普通的控件,那么就可以使用SliverToBoxAdapter来将各种视图组合在一起,放在CustomListView中。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 
 | CustomScrollView(physics: ScrollPhysics(),
 slivers: <Widget>[
 SliverToBoxAdapter(
 child: _buildHeader(),
 ),
 SliverGrid.count(
 crossAxisCount: 3,
 children: products.map((product) {
 return _buildItemGrid(product);
 }).toList(),
 ),
 SliverToBoxAdapter(
 child: _buildSearch(),
 ),
 SliverFixedExtentList(
 itemExtent: 100.0,
 delegate: SliverChildListDelegate(
 products.map((product) {
 return _buildItemList(product);
 }).toList(),
 ),
 ),
 SliverToBoxAdapter(
 child: _buildFooter(),
 ),
 ],
 );
 
 |