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了,如果直接将ListView
、GridView
作为CustomScrollView
是不行的,因为它们本身是可滚动组件而并不是Sliver!因此,为了能让可滚动组件能和CustomScrollView
配合使用,Flutter提供了一些可滚动组件的Sliver版,如SliverList、SliverGrid等。
实际上Sliver版的可滚动组件和非Sliver版的可滚动组件最大的区别就是前者不包含滚动模型(自身不能再滚动),而后者包含滚动模型 ,也正因如此,CustomScrollView
才可以将多个Sliver”粘”在一起,这些Sliver共用CustomScrollView
的Scrollable
,所以最终才实现了统一的滑动效果。
Sliver系列Widget比较多,我们不会一一介绍,读者只需记住它的特点,需要时再去查看文档即可。上面之所以说“大多数”Sliver都和可滚动组件对应,是由于还有一些如SliverPadding、SliverAppBar等是和可滚动组件无关的,它们主要是为了结合CustomScrollView一起使用,这是因为CustomScrollView的子组件必须都是Sliver。
API 定义
CustomScrollView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
Key center,
double anchor = 0.0,
double cacheExtent,
this.slivers = const <Widget>[],
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
String restorationId,
Clip clipBehavior = Clip.hardEdge,
})
示例1:相互嵌套场景
在实际业务场景中经常见到这样的布局,顶部是网格布局(GridView),然后是列表布局(ListView),滚动的时候做为一个整体,此场景是无法使用GridView+ListView来实现的,而是需要使用CustomScrollView+SliverGrid+SliverList来实现,实现代码如下:
CustomScrollView(
slivers: [
SliverGrid.count(
crossAxisCount: 4,
childAspectRatio: 2,
children: List.generate(
8,
(index) => Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Text('$index', textScaleFactor: 2),
),
).toList(),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
height: 100,
color: Colors.primaries[index % Colors.primaries.length],
child: Text('$index', textScaleFactor: 2),
),
childCount: 20,
),
),
],
),
示例2:顶部AppBar吸顶示例
实际项目中页面顶部是AppBar,然后是GridView,最后是ListView,这3个区域以整体来滚动,AppBar具有吸顶效果,此效果也是我们经常遇到的,用法如下:
CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 230,
flexibleSpace: FlexibleSpaceBar(
title: Text('我是吸顶后的标题'),
background: Image.network(
'https://images.h128.com/upload/202012/20/202012201302083641.jpg',
fit: BoxFit.cover,
),
),
),
// 下面代码同示例1
SliverGrid.count(
...
),
SliverList(
...
),
],
),
示例3:Sliver-Sticky效果
tab1.dart
import 'package:flutter/material.dart';
import 'package:app1/demos/tabs/tab1_view1.dart';
import 'package:app1/demos/tabs/tab1_view2.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
class Tab1 extends StatefulWidget {
@override
_Tab1State createState() => _Tab1State();
}
class _Tab1State extends State<Tab1> with SingleTickerProviderStateMixin {
ScrollController _scrollViewController;
int _tabIndex = 0;
List<Map> _list = [
{
'index': 0,
'title': 'tab1',
'key': tab1View1ChildKey,
'child': Tab1View1(key: tab1View1ChildKey)
},
{
'index': 1,
'title': 'tab2',
'key': tab1View2ChildKey,
'child': Tab1View2(key: tab1View2ChildKey)
},
];
@override
void initState() {
super.initState();
_scrollViewController = ScrollController();
// 监听滚动事件,打印滚动位置
// _scrollViewController.addListener(() {
// print(_scrollViewController.offset);
// });
}
@override
Widget build(BuildContext context) {
print('_tabIndex $_tabIndex');
var currentTab = _list[_tabIndex];
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
child: EasyRefresh.custom(
scrollController: _scrollViewController,
firstRefresh: true, // 是否需要首次刷新
firstRefreshWidget: Center(child: Text('页面加载中...')), // 首次刷新加载组件
onRefresh: () async {
await Future.delayed(Duration(seconds: 2), () {
if (mounted) {
var tabViewChild = currentTab['key'].currentState;
tabViewChild.getData(isRefresh: true);
}
});
},
onLoad: () async {
await Future.delayed(Duration(seconds: 2), () {
if (mounted) {
// var tabViewChild = currentTab['key'].currentState;
// tabViewChild.getData(isRefresh: false, onSuccess);
}
});
},
slivers: [
SliverAppBar(
pinned: true,
elevation: 0,
expandedHeight: 250,
backgroundColor: Colors.green,
flexibleSpace: FlexibleSpaceBar(
title: Text('Sliver-sticky效果'),
),
),
SliverList(
delegate: SliverChildListDelegate([
Container(
height: 200,
color: Colors.red,
child: Text('asdfs'),
),
]),
),
SliverPersistentHeader(
pinned: true,
delegate: _buildPersistentHeader(
child: Container(
color: Colors.yellow,
height: 60,
child: Row(
children: _list
.map(
(item) => InkWell(
child: Text(item['title']),
onTap: () {
setState(() {
_tabIndex = item['index'];
_scrollViewController.jumpTo(0); //切换tab后,跳回顶部
});
},
),
)
.toList(),
),
),
),
),
SliverToBoxAdapter(
child: Container(
height: 1000.0,
child: currentTab['child'],
),
),
],
),
),
);
}
}
class _buildPersistentHeader extends SliverPersistentHeaderDelegate {
final child;
_buildPersistentHeader({@required this.child});
// shrinkOffset 偏移量
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return this.child;
}
// 最大上滑高度
@override
// double get maxExtent => this.child.preferredSize.height;
double get maxExtent => 60;
// 最小上滑高度
@override
// double get minExtent => this.child.preferredSize.height;
double get minExtent => 60;
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
tab1_view1.dart
import 'package:flutter/material.dart';
GlobalKey<_Tab1View1State> tab1View1ChildKey = GlobalKey();
class Tab1View1 extends StatefulWidget {
Tab1View1({Key key}) : super(key: key);
@override
_Tab1View1State createState() => _Tab1View1State();
}
class _Tab1View1State extends State<Tab1View1> {
@override
void initState() {
super.initState();
print('view1-initState');
}
@override
void dispose() {
print('view2-dispose');
super.dispose();
}
getData({isRefresh = false}) {
print('view1-getData');
}
@override
Widget build(BuildContext context) {
return Container(
child: Text('view1'),
);
}
}