https://docs.flutter.io/flutter/widgets/GridView-class.html

CustomScrollView是可以使用Sliver来自定义滚动模型(效果)的组件。它可以包含多种滚动模型。

使用场景:

  • 有一个页面,顶部需要一个GridView,底部需要一个ListView,而要求整个页面的滑动效果是统一的,即它们看起来是一个整体。如果使用GridView+ListView来实现的话,就不能保证一致的滑动效果,因为它们的滚动效果是分离的,所以这时就需要一个”胶水”,把这些彼此独立的可滚动组件”粘”起来。
  • ListView和GridView相互嵌套场景,ListView嵌套GridView时,需要给GridView指定高度,但我们希望高度随内容而变化(不指定),ListView和GridView使用同一个滚动效果。
  • 一个页面顶部是AppBar,然后是GridView,最后是ListView,这3个区域以整体来滚动,AppBar具有吸顶效果。

CustomScrollView的功能就相当于“胶水”,将多个组件粘合在一起,具统一的滚动效果。

可滚动组件的Sliver版

Sliver有细片、薄片之意,在Flutter中,Sliver通常指可滚动组件子元素(就像一个个薄片一样)。但是在CustomScrollView中,需要粘起来的可滚动组件就是CustomScrollView的Sliver了,如果直接将ListViewGridView作为CustomScrollView是不行的,因为它们本身是可滚动组件而并不是Sliver!因此,为了能让可滚动组件能和CustomScrollView配合使用,Flutter提供了一些可滚动组件的Sliver版,如SliverList、SliverGrid等。

实际上Sliver版的可滚动组件和非Sliver版的可滚动组件最大的区别就是前者不包含滚动模型(自身不能再滚动),而后者包含滚动模型 ,也正因如此,CustomScrollView才可以将多个Sliver”粘”在一起,这些Sliver共用CustomScrollViewScrollable,所以最终才实现了统一的滑动效果。

Sliver系列Widget比较多,我们不会一一介绍,读者只需记住它的特点,需要时再去查看文档即可。上面之所以说“大多数”Sliver都和可滚动组件对应,是由于还有一些如SliverPadding、SliverAppBar等是和可滚动组件无关的,它们主要是为了结合CustomScrollView一起使用,这是因为CustomScrollView的子组件必须都是Sliver

API 定义

  1. CustomScrollView({
  2. Key key,
  3. Axis scrollDirection = Axis.vertical,
  4. bool reverse = false,
  5. ScrollController controller,
  6. bool primary,
  7. ScrollPhysics physics,
  8. bool shrinkWrap = false,
  9. Key center,
  10. double anchor = 0.0,
  11. double cacheExtent,
  12. this.slivers = const <Widget>[],
  13. int semanticChildCount,
  14. DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  15. String restorationId,
  16. Clip clipBehavior = Clip.hardEdge,
  17. })

示例1:相互嵌套场景

在实际业务场景中经常见到这样的布局,顶部是网格布局(GridView),然后是列表布局(ListView),滚动的时候做为一个整体,此场景是无法使用GridView+ListView来实现的,而是需要使用CustomScrollView+SliverGrid+SliverList来实现,实现代码如下:
image.png

  1. CustomScrollView(
  2. slivers: [
  3. SliverGrid.count(
  4. crossAxisCount: 4,
  5. childAspectRatio: 2,
  6. children: List.generate(
  7. 8,
  8. (index) => Container(
  9. color: Colors.primaries[index % Colors.primaries.length],
  10. child: Text('$index', textScaleFactor: 2),
  11. ),
  12. ).toList(),
  13. ),
  14. SliverList(
  15. delegate: SliverChildBuilderDelegate(
  16. (context, index) => Container(
  17. height: 100,
  18. color: Colors.primaries[index % Colors.primaries.length],
  19. child: Text('$index', textScaleFactor: 2),
  20. ),
  21. childCount: 20,
  22. ),
  23. ),
  24. ],
  25. ),

示例2:顶部AppBar吸顶示例

实际项目中页面顶部是AppBar,然后是GridView,最后是ListView,这3个区域以整体来滚动,AppBar具有吸顶效果,此效果也是我们经常遇到的,用法如下:
s.gif

  1. CustomScrollView(
  2. slivers: [
  3. SliverAppBar(
  4. pinned: true,
  5. expandedHeight: 230,
  6. flexibleSpace: FlexibleSpaceBar(
  7. title: Text('我是吸顶后的标题'),
  8. background: Image.network(
  9. 'https://images.h128.com/upload/202012/20/202012201302083641.jpg',
  10. fit: BoxFit.cover,
  11. ),
  12. ),
  13. ),
  14. // 下面代码同示例1
  15. SliverGrid.count(
  16. ...
  17. ),
  18. SliverList(
  19. ...
  20. ),
  21. ],
  22. ),

示例3:Sliver-Sticky效果

fdtdt.gif

tab1.dart
  1. import 'package:flutter/material.dart';
  2. import 'package:app1/demos/tabs/tab1_view1.dart';
  3. import 'package:app1/demos/tabs/tab1_view2.dart';
  4. import 'package:flutter_easyrefresh/easy_refresh.dart';
  5. class Tab1 extends StatefulWidget {
  6. @override
  7. _Tab1State createState() => _Tab1State();
  8. }
  9. class _Tab1State extends State<Tab1> with SingleTickerProviderStateMixin {
  10. ScrollController _scrollViewController;
  11. int _tabIndex = 0;
  12. List<Map> _list = [
  13. {
  14. 'index': 0,
  15. 'title': 'tab1',
  16. 'key': tab1View1ChildKey,
  17. 'child': Tab1View1(key: tab1View1ChildKey)
  18. },
  19. {
  20. 'index': 1,
  21. 'title': 'tab2',
  22. 'key': tab1View2ChildKey,
  23. 'child': Tab1View2(key: tab1View2ChildKey)
  24. },
  25. ];
  26. @override
  27. void initState() {
  28. super.initState();
  29. _scrollViewController = ScrollController();
  30. // 监听滚动事件,打印滚动位置
  31. // _scrollViewController.addListener(() {
  32. // print(_scrollViewController.offset);
  33. // });
  34. }
  35. @override
  36. Widget build(BuildContext context) {
  37. print('_tabIndex $_tabIndex');
  38. var currentTab = _list[_tabIndex];
  39. return Scaffold(
  40. body: Container(
  41. width: double.infinity,
  42. height: double.infinity,
  43. child: EasyRefresh.custom(
  44. scrollController: _scrollViewController,
  45. firstRefresh: true, // 是否需要首次刷新
  46. firstRefreshWidget: Center(child: Text('页面加载中...')), // 首次刷新加载组件
  47. onRefresh: () async {
  48. await Future.delayed(Duration(seconds: 2), () {
  49. if (mounted) {
  50. var tabViewChild = currentTab['key'].currentState;
  51. tabViewChild.getData(isRefresh: true);
  52. }
  53. });
  54. },
  55. onLoad: () async {
  56. await Future.delayed(Duration(seconds: 2), () {
  57. if (mounted) {
  58. // var tabViewChild = currentTab['key'].currentState;
  59. // tabViewChild.getData(isRefresh: false, onSuccess);
  60. }
  61. });
  62. },
  63. slivers: [
  64. SliverAppBar(
  65. pinned: true,
  66. elevation: 0,
  67. expandedHeight: 250,
  68. backgroundColor: Colors.green,
  69. flexibleSpace: FlexibleSpaceBar(
  70. title: Text('Sliver-sticky效果'),
  71. ),
  72. ),
  73. SliverList(
  74. delegate: SliverChildListDelegate([
  75. Container(
  76. height: 200,
  77. color: Colors.red,
  78. child: Text('asdfs'),
  79. ),
  80. ]),
  81. ),
  82. SliverPersistentHeader(
  83. pinned: true,
  84. delegate: _buildPersistentHeader(
  85. child: Container(
  86. color: Colors.yellow,
  87. height: 60,
  88. child: Row(
  89. children: _list
  90. .map(
  91. (item) => InkWell(
  92. child: Text(item['title']),
  93. onTap: () {
  94. setState(() {
  95. _tabIndex = item['index'];
  96. _scrollViewController.jumpTo(0); //切换tab后,跳回顶部
  97. });
  98. },
  99. ),
  100. )
  101. .toList(),
  102. ),
  103. ),
  104. ),
  105. ),
  106. SliverToBoxAdapter(
  107. child: Container(
  108. height: 1000.0,
  109. child: currentTab['child'],
  110. ),
  111. ),
  112. ],
  113. ),
  114. ),
  115. );
  116. }
  117. }
  118. class _buildPersistentHeader extends SliverPersistentHeaderDelegate {
  119. final child;
  120. _buildPersistentHeader({@required this.child});
  121. // shrinkOffset 偏移量
  122. @override
  123. Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
  124. return this.child;
  125. }
  126. // 最大上滑高度
  127. @override
  128. // double get maxExtent => this.child.preferredSize.height;
  129. double get maxExtent => 60;
  130. // 最小上滑高度
  131. @override
  132. // double get minExtent => this.child.preferredSize.height;
  133. double get minExtent => 60;
  134. @override
  135. bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
  136. return true;
  137. }
  138. }

tab1_view1.dart

  1. import 'package:flutter/material.dart';
  2. GlobalKey<_Tab1View1State> tab1View1ChildKey = GlobalKey();
  3. class Tab1View1 extends StatefulWidget {
  4. Tab1View1({Key key}) : super(key: key);
  5. @override
  6. _Tab1View1State createState() => _Tab1View1State();
  7. }
  8. class _Tab1View1State extends State<Tab1View1> {
  9. @override
  10. void initState() {
  11. super.initState();
  12. print('view1-initState');
  13. }
  14. @override
  15. void dispose() {
  16. print('view2-dispose');
  17. super.dispose();
  18. }
  19. getData({isRefresh = false}) {
  20. print('view1-getData');
  21. }
  22. @override
  23. Widget build(BuildContext context) {
  24. return Container(
  25. child: Text('view1'),
  26. );
  27. }
  28. }