想象有一种应用场景,scrollable组件内部嵌套另一个scrollable组件。特别是当同时显示ListView和GridView时应该怎么处理。
1 2 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来实现这一需求。通过以下代码替换:
1 2 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属性
1 2 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属性就可以,可以滑动的列表,常常用于滑动组件嵌套的场景
1 2 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来构建
1 2 3 4 5 6 7 8 9 10 11
| CustomScrollView( slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate([ _buildItem(), _buildItem(), _buildItem(), ]), ) ], );
|
SliverChildListDelegate和SliverChildBuilderDelegate的区别:
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怎么排列
1 2 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会随着滑动而展开/收起
1 2 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类:
1 2 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中。
1 2 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(), ), ], );
|