CustomScrollView

CustomScrollView 是可以使用 Sliver 来自定义滚动模型(效果)的组件。它可以包含多种滚动模型,举个例子,假设有一个页面,顶部需要一个 GridView,底部需要一个 ListView,而要求整个页面的滑动效果是统一的,即它们看起来是一个整体。如果使用 GridView+ListView 来实现的话,就不能保证一致的滑动效果,因为它们的滚动效果是分离的,所以这时就需要一个”胶水”,把这些彼此独立的可滚动组件”粘”起来,而 CustomScrollView 的功能就相当于“胶水”。

Slivers

Flutter中的 Slivers 大家族基本都是配合CustomScrollView来实现的,除了上面提到的滑动布局嵌套,你还可以使用Slivers来实现页面头部展开/收起、 AppBar随手势变换等等功能。

SliverAppBar

如果你是一名Android开发者,一定使用过CollapsingToolbarLayout这个布局来实现AppBar展开/收起的功能,在Flutter里面则对应SliverAppBar控件。给 SliverAppBar 设置flexibleSpace和expandedHeight属性,就可以轻松完成AppBar展开/收起的功能:

  1. CustomScrollView(
  2. slivers: <Widget>[
  3. SliverAppBar(
  4. actions: <Widget>[
  5. FlatButton(
  6. child: Text('click me', style: TextStyle(
  7. fontSize: 20,
  8. color: Colors.white
  9. ),),
  10. ),
  11. ],
  12. title: Text('SliverAppBar'),
  13. backgroundColor: Theme.of(context).accentColor,
  14. expandedHeight: 200.0,
  15. flexibleSpace: FlexibleSpaceBar(
  16. background: Image.asset('assets/imgs/bg.jpg', fit: BoxFit.cover),
  17. ),
  18. // floating: true,
  19. // snap: true,
  20. // pinned: true,
  21. ),
  22. SliverFixedExtentList(
  23. itemExtent: 120.0,
  24. delegate: SliverChildListDelegate(
  25. [1,2,3,4,5,6,7].map((product) {
  26. return Container(
  27. alignment: Alignment.center,
  28. child: new Text('list item $product'),
  29. );
  30. }).toList(),
  31. ),
  32. ),
  33. ],
  34. )

效果:
002.gif

如果设置floating属性为true,那么AppBar会在你做出下拉手势时就立即展开(即使ListView并没有到达顶部),该展开状态不显示flexibleSpace:
003.gif
如果同时设置floating和snap属性为true,那么AppBar会在你做出下拉手势时就立即全部展开(即使ListView并没有到达顶部),该展开状态显示flexibleSpace:
004.gif
如果不想AppBar消失,则设置pinned属性为true即可。
005.gif

SliverList

SliverList 的使用非常简单,只需设置delegate属性即可,我们一般使用SliverChildBuilderDelegate,注意记得设置childCount,否则Flutter没法知道怎么绘制:

  1. CustomScrollView(
  2. slivers: <Widget>[
  3. SliverList(
  4. delegate: SliverChildBuilderDelegate(
  5. (BuildContext context, int index) {
  6. return _buildItem(context, products[index]);
  7. },
  8. childCount: 3,
  9. ),
  10. )
  11. ],
  12. );

也可以通过下面的方式来设置childCount,如果不设置childCount,Flutter一旦发现delegate的某个index返回了null,就会认为childCount就是这个index。

  1. SliverChildBuilderDelegate(
  2. (BuildContext context, int index) {
  3. if(index > products.length){
  4. return null; // 返回null则将此index设置为childCount
  5. }
  6. return _buildItem(context, products[index]);
  7. },

也可以使用SliverChildListDelegate来构建delegate:

  1. SliverChildListDelegate([
  2. _buildItem(),
  3. _buildItem(),
  4. _buildItem(),
  5. ]),

SliverFixedExtentList

SliverFixedExtentList 可以为列表的每一项指定高度(itemExtent):

  1. new SliverFixedExtentList(
  2. itemExtent: 50.0,
  3. delegate: new SliverChildBuilderDelegate(
  4. (BuildContext context, int index) {
  5. return new Container(
  6. alignment: Alignment.center,
  7. color: Colors.lightBlue[100 * (index % 9)],
  8. child: new Text('list item $index'),
  9. );
  10. },
  11. childCount: 50
  12. ),
  13. ),

:::info SliverFixedExtentList 跟 SliverList 不同的一点是, SliverFixedExtentList 的高度固定为其 itemExtent 属性值, 而 SliverList 的高度是自动的, 取决于其 delegate 中子元素的高度 :::

SliverPrototypeExtentList

SliverPrototypeExtentList 将其子项排列在沿着主轴的一条线上,从零偏移开始,没有间隙。每个子项的约束程度与沿主轴的prototypeItem和沿横轴的SliverConstraints.crossAxisExtent的程度相同。

SliverChildListDelegate 和 SliverChildBuilderDelegate 的区别

  • SliverChildListDelegate 一般用来构item建数量明确的列表,会提前build好所有的子item,所以在效率上会有问题,适合item数量不多的情况(不超过一屏)。
  • SliverChildBuilderDelegate 构建的列表理论上是可以无限长的,因为使用来lazily construct优化。
    (两者的区别有些类似于ListView和ListView.builder()的区别。)

SliverGrid

SliverGrid 有三个构造函数:SliverGrid.count()SliverGrid.extentSliverGrid()

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. new SliverGrid(
  2. gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
  3. crossAxisCount: 2, //Grid按两列显示
  4. mainAxisSpacing: 10.0,
  5. crossAxisSpacing: 10.0,
  6. childAspectRatio: 4.0,
  7. ),
  8. delegate: new SliverChildBuilderDelegate(
  9. (BuildContext context, int index) {
  10. return new Container(
  11. alignment: Alignment.center,
  12. color: Colors.cyan[100 * (index % 9)],
  13. child: new Text('grid item $index'),
  14. );
  15. },
  16. childCount: 20,
  17. ),
  18. ),

SliverPersistentHeader

SliverPersistentHeader 顾名思义,就是给一个可滑动的视图添加一个头(实际上,在CustomScrollView的slivers列表中,header可以出现在视图的任意位置,不一定要是在顶部)。这个Header会随着滑动而展开/收起,使用pinned和floating属性来控制收起时Header是否展示(pinned和floating属性不可以同时为true),pinned和floating属性的具体意义和SliverAppBar中相同。

  1. SliverPersistentHeader(
  2. pinned: true,
  3. delegate: _SliverAppBarDelegate(
  4. minHeight: 60.0,
  5. maxHeight: 180.0,
  6. child: Container(
  7. child: Image.asset(
  8. "assets/imgs/bg.jpg", fit: BoxFit.fitWidth,),
  9. ),
  10. ),
  11. ),

效果:
006.gif

构建一个 SliverPersistentHeader 需要传入一个delegate,这个delegate是 SliverPersistentHeaderDelegate 类型的,而 SliverPersistentHeaderDelegate 是一个abstract类,我们不能直接new一个 SliverPersistentHeaderDelegate 出来,因此,我们需要自定义一个 delegate 来实现 SliverPersistentHeaderDelegate 类:

  1. class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  2. _SliverAppBarDelegate({
  3. @required this.minHeight,
  4. @required this.maxHeight,
  5. @required this.child,
  6. });
  7. final double minHeight;
  8. final double maxHeight;
  9. final Widget child;
  10. @override
  11. double get minExtent => minHeight;
  12. @override
  13. double get maxExtent => maxHeight;
  14. @override
  15. Widget build(
  16. BuildContext context, double shrinkOffset, bool overlapsContent) {
  17. return new SizedBox.expand(child: child);
  18. }
  19. @override
  20. bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
  21. return maxHeight != oldDelegate.maxHeight ||
  22. minHeight != oldDelegate.minHeight ||
  23. child != oldDelegate.child;
  24. }
  25. }

写一个自定义SliverPersistentHeaderDelegate很简单,只需重写build()、get maxExtent、get minExtent和shouldRebuild()这四个方法,上面就是一个最简单的SliverPersistentHeaderDelegate的实现。其中,maxExtent表示header完全展开时的高度,minExtent表示header在收起时的最小高度。因此,对于我们上面的那个自定义Delegate,如果将minHeight和maxHeight的值设置为相同时,header就不会收缩了,这样的Header跟我们平常理解的Header更像。

SliverPadding

SliverPadding 可以为 sliver 添加间距

  1. SliverPadding(
  2. padding: const EdgeInsets.all(8.0),
  3. sliver: new SliverGrid(
  4. // ...
  5. ),
  6. ),

SliverToBoxAdapter

那么如果想要在滚动视图中添加一个普通的控件,那么就可以使用 SliverToBoxAdapter 来将各种视图组合在一起,放在 CustomListView 中。

结合SliverToBoxAdapter,滚动视图可以任意组合:

  1. CustomScrollView(
  2. physics: ScrollPhysics(),
  3. slivers: <Widget>[
  4. SliverToBoxAdapter(
  5. child: Placeholder(fallbackHeight: 100,),
  6. ),
  7. SliverGrid.count(
  8. crossAxisCount: 3,
  9. children: products.map((product) {
  10. return _buildItemGrid(product);
  11. }).toList(),
  12. ),
  13. SliverToBoxAdapter(
  14. child: Placeholder(fallbackHeight: 100,),
  15. ),
  16. SliverFixedExtentList(
  17. itemExtent: 100.0,
  18. delegate: SliverChildListDelegate(
  19. products.map((product) {
  20. return _buildItemList(product);
  21. }).toList(),
  22. ),
  23. ),
  24. ],
  25. );

SliverFillViewport

SliverFillViewport 占满一屏或者比一屏更多的布局

  1. new SliverFillViewport(
  2. delegate: new SliverChildBuilderDelegate(
  3. (BuildContext context, int index) {
  4. //创建列表项
  5. return new Container(
  6. alignment: Alignment.center,
  7. color: Colors.lightBlue,
  8. child: new Text('SliverFillViewport'),
  9. );
  10. },
  11. childCount: 1
  12. ),
  13. viewportFraction: 1.0, // 占屏幕的比例
  14. ),

SliverFillRemaining

SliverFillRemaining 用于填充完剩余视图里面的全部空间, 详见 示例2

SliverSafeArea

SliverSafeArea 通过足够的填充来插入另一条条子以防止操作系统入侵的条子。例如,这将使条子缩进足以避开屏幕顶部的状态栏。为了防止各种边界的越界,比如说越过顶部的状态栏

示例1: 基础用法

  1. import 'package:flutter/material.dart';
  2. void main() {
  3. runApp(new StartApp());
  4. }
  5. class StartApp extends StatefulWidget {
  6. @override
  7. State<StatefulWidget> createState() {
  8. return new _StartAppState();
  9. }
  10. }
  11. class _StartAppState extends State<StartApp> {
  12. @override
  13. Widget build(BuildContext context) {
  14. return MaterialApp(
  15. title: '首页',
  16. theme: new ThemeData(
  17. primarySwatch: Colors.blue,
  18. ),
  19. home: CustomScrollViewTestRoute(),
  20. );
  21. }
  22. }
  23. class CustomScrollViewTestRoute extends StatelessWidget {
  24. @override
  25. Widget build(BuildContext context) {
  26. // 因为本路由没有使用Scaffold,为了让子级Widget(如Text)使用
  27. // Material Design 默认的样式风格,我们使用Material作为本路由的根。
  28. return Material(
  29. child: CustomScrollView(
  30. slivers: <Widget>[
  31. SliverAppBar(
  32. actions: <Widget>[
  33. FlatButton(
  34. child: Text('click me', style: TextStyle(
  35. fontSize: 20,
  36. color: Colors.white
  37. ),),
  38. ),
  39. ],
  40. backgroundColor: Theme.of(context).accentColor,
  41. expandedHeight: 200.0,
  42. flexibleSpace: FlexibleSpaceBar(
  43. background: Image.asset('assets/imgs/bg.jpg', fit: BoxFit.cover),
  44. title: Text('Demo'),
  45. ),
  46. pinned: true,
  47. ),
  48. SliverFixedExtentList(
  49. itemExtent: 120.0,
  50. delegate: SliverChildListDelegate(
  51. [1,2,3,4,5,6,7].map((product) {
  52. return Container(
  53. alignment: Alignment.center,
  54. child: new Text('list item $product'),
  55. );
  56. }).toList(),
  57. ),
  58. ),
  59. SliverPersistentHeader(
  60. pinned: true,
  61. delegate: _SliverAppBarDelegate(
  62. minHeight: 60.0,
  63. maxHeight: 180.0,
  64. child: Container(
  65. child: Image.asset(
  66. "assets/imgs/bg.jpg", fit: BoxFit.fitWidth,),
  67. ),
  68. ),
  69. ),
  70. SliverPadding(
  71. padding: const EdgeInsets.all(8.0),
  72. sliver: new SliverGrid( //Grid
  73. gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
  74. crossAxisCount: 2, //Grid按两列显示
  75. mainAxisSpacing: 10.0,
  76. crossAxisSpacing: 10.0,
  77. childAspectRatio: 4.0,
  78. ),
  79. delegate: new SliverChildBuilderDelegate(
  80. (BuildContext context, int index) {
  81. //创建子widget
  82. return new Container(
  83. alignment: Alignment.center,
  84. color: Colors.cyan[100 * (index % 9)],
  85. child: new Text('grid item $index'),
  86. );
  87. },
  88. childCount: 20,
  89. ),
  90. ),
  91. ),
  92. SliverToBoxAdapter(
  93. child: Placeholder(fallbackHeight: 100,),
  94. ),
  95. new SliverFixedExtentList(
  96. itemExtent: 50.0,
  97. delegate: new SliverChildBuilderDelegate(
  98. (BuildContext context, int index) {
  99. //创建列表项
  100. return new Container(
  101. alignment: Alignment.center,
  102. color: Colors.lightBlue[100 * (index % 9)],
  103. child: new Text('list item $index'),
  104. );
  105. },
  106. childCount: 50 //50个列表项
  107. ),
  108. ),
  109. ],
  110. )
  111. );
  112. }
  113. }
  114. class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  115. _SliverAppBarDelegate({
  116. @required this.minHeight,
  117. @required this.maxHeight,
  118. @required this.child,
  119. });
  120. final double minHeight;
  121. final double maxHeight;
  122. final Widget child;
  123. @override
  124. double get minExtent => minHeight;
  125. @override
  126. double get maxExtent => maxHeight;
  127. @override
  128. Widget build(
  129. BuildContext context, double shrinkOffset, bool overlapsContent) {
  130. return new SizedBox.expand(child: child);
  131. }
  132. @override
  133. bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
  134. return maxHeight != oldDelegate.maxHeight ||
  135. minHeight != oldDelegate.minHeight ||
  136. child != oldDelegate.child;
  137. }
  138. }

效果:
001.gif

示例2: Sliver-sticky效果

  1. class _StickyDemoState extends State<StickyDemo>
  2. with SingleTickerProviderStateMixin {
  3. TabController tabController;
  4. @override
  5. void initState() {
  6. super.initState();
  7. this.tabController = TabController(length: 2, vsync: this);
  8. }
  9. @override
  10. Widget build(BuildContext context) {
  11. return Scaffold(
  12. body: CustomScrollView(
  13. slivers: <Widget>[
  14. SliverAppBar(
  15. pinned: true,
  16. elevation: 0,
  17. expandedHeight: 250,
  18. flexibleSpace: FlexibleSpaceBar(
  19. title: Text('Sliver-sticky效果'),
  20. background: Image.network(
  21. 'http://img1.mukewang.com/5c18cf540001ac8206000338.jpg',
  22. fit: BoxFit.cover,
  23. ),
  24. ),
  25. ),
  26. SliverPersistentHeader(
  27. pinned: true,
  28. delegate: StickyTabBarDelegate(
  29. child: TabBar(
  30. labelColor: Colors.black,
  31. controller: this.tabController,
  32. tabs: <Widget>[
  33. Tab(text: 'Home'),
  34. Tab(text: 'Profile'),
  35. ],
  36. ),
  37. ),
  38. ),
  39. SliverFillRemaining(
  40. child: TabBarView(
  41. controller: this.tabController,
  42. children: <Widget>[
  43. Center(child: Text('Content of Home')),
  44. Center(child: Text('Content of Profile')),
  45. ],
  46. ),
  47. ),
  48. ],
  49. ),
  50. );
  51. }
  52. }
  53. class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  54. final TabBar child;
  55. StickyTabBarDelegate({@required this.child});
  56. @override
  57. Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
  58. return this.child;
  59. }
  60. @override
  61. double get maxExtent => this.child.preferredSize.height;
  62. @override
  63. double get minExtent => this.child.preferredSize.height;
  64. @override
  65. bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
  66. return true;
  67. }
  68. }

效果:
007.gif

参考资料