CustomScrollView
CustomScrollView 是可以使用 Sliver 来自定义滚动模型(效果)的组件。它可以包含多种滚动模型,举个例子,假设有一个页面,顶部需要一个 GridView,底部需要一个 ListView,而要求整个页面的滑动效果是统一的,即它们看起来是一个整体。如果使用 GridView+ListView 来实现的话,就不能保证一致的滑动效果,因为它们的滚动效果是分离的,所以这时就需要一个”胶水”,把这些彼此独立的可滚动组件”粘”起来,而 CustomScrollView 的功能就相当于“胶水”。
Slivers
Flutter中的 Slivers 大家族基本都是配合CustomScrollView来实现的,除了上面提到的滑动布局嵌套,你还可以使用Slivers来实现页面头部展开/收起、 AppBar随手势变换等等功能。
SliverAppBar
如果你是一名Android开发者,一定使用过CollapsingToolbarLayout这个布局来实现AppBar展开/收起的功能,在Flutter里面则对应SliverAppBar控件。给 SliverAppBar 设置flexibleSpace和expandedHeight属性,就可以轻松完成AppBar展开/收起的功能:
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
actions: <Widget>[
FlatButton(
child: Text('click me', style: TextStyle(
fontSize: 20,
color: Colors.white
),),
),
],
title: Text('SliverAppBar'),
backgroundColor: Theme.of(context).accentColor,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.asset('assets/imgs/bg.jpg', fit: BoxFit.cover),
),
// floating: true,
// snap: true,
// pinned: true,
),
SliverFixedExtentList(
itemExtent: 120.0,
delegate: SliverChildListDelegate(
[1,2,3,4,5,6,7].map((product) {
return Container(
alignment: Alignment.center,
child: new Text('list item $product'),
);
}).toList(),
),
),
],
)
效果:
如果设置floating属性为true,那么AppBar会在你做出下拉手势时就立即展开(即使ListView并没有到达顶部),该展开状态不显示flexibleSpace:
如果同时设置floating和snap属性为true,那么AppBar会在你做出下拉手势时就立即全部展开(即使ListView并没有到达顶部),该展开状态显示flexibleSpace:
如果不想AppBar消失,则设置pinned属性为true即可。
SliverList
SliverList 的使用非常简单,只需设置delegate属性即可,我们一般使用SliverChildBuilderDelegate,注意记得设置childCount,否则Flutter没法知道怎么绘制:
CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _buildItem(context, products[index]);
},
childCount: 3,
),
)
],
);
也可以通过下面的方式来设置childCount,如果不设置childCount,Flutter一旦发现delegate的某个index返回了null,就会认为childCount就是这个index。
SliverChildBuilderDelegate(
(BuildContext context, int index) {
if(index > products.length){
return null; // 返回null则将此index设置为childCount
}
return _buildItem(context, products[index]);
},
也可以使用SliverChildListDelegate来构建delegate:
SliverChildListDelegate([
_buildItem(),
_buildItem(),
_buildItem(),
]),
SliverFixedExtentList
SliverFixedExtentList 可以为列表的每一项指定高度(itemExtent):
new SliverFixedExtentList(
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 50
),
),
:::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.extent
和SliverGrid()
SliverGrid.count()
指定了一行展示多少个item,下面的例子表示一行展示4个:
SliverGrid.count(children: scrollItems, crossAxisCount: 4)
SliverGrid.extent
可以指定item的最大宽度,然后让Flutter自己决定一行展示多少个item:
SliverGrid.extent(children: scrollItems, maxCrossAxisExtent: 90.0)
SliverGrid()
则是需要指定一个gridDelegate,它提供给了程序员一个自定义Delegate的入口,你可以自己决定每一个item怎么排列:
new SliverGrid(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //Grid按两列显示
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.cyan[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
SliverPersistentHeader
SliverPersistentHeader 顾名思义,就是给一个可滑动的视图添加一个头(实际上,在CustomScrollView的slivers列表中,header可以出现在视图的任意位置,不一定要是在顶部)。这个Header会随着滑动而展开/收起,使用pinned和floating属性来控制收起时Header是否展示(pinned和floating属性不可以同时为true),pinned和floating属性的具体意义和SliverAppBar中相同。
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 180.0,
child: Container(
child: Image.asset(
"assets/imgs/bg.jpg", fit: BoxFit.fitWidth,),
),
),
),
效果:
构建一个 SliverPersistentHeader 需要传入一个delegate,这个delegate是 SliverPersistentHeaderDelegate 类型的,而 SliverPersistentHeaderDelegate 是一个abstract类,我们不能直接new一个 SliverPersistentHeaderDelegate 出来,因此,我们需要自定义一个 delegate 来实现 SliverPersistentHeaderDelegate 类:
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 => maxHeight;
@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更像。
SliverPadding
SliverPadding 可以为 sliver 添加间距
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: new SliverGrid(
// ...
),
),
SliverToBoxAdapter
那么如果想要在滚动视图中添加一个普通的控件,那么就可以使用 SliverToBoxAdapter 来将各种视图组合在一起,放在 CustomListView 中。
结合SliverToBoxAdapter,滚动视图可以任意组合:
CustomScrollView(
physics: ScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: Placeholder(fallbackHeight: 100,),
),
SliverGrid.count(
crossAxisCount: 3,
children: products.map((product) {
return _buildItemGrid(product);
}).toList(),
),
SliverToBoxAdapter(
child: Placeholder(fallbackHeight: 100,),
),
SliverFixedExtentList(
itemExtent: 100.0,
delegate: SliverChildListDelegate(
products.map((product) {
return _buildItemList(product);
}).toList(),
),
),
],
);
SliverFillViewport
SliverFillViewport 占满一屏或者比一屏更多的布局
new SliverFillViewport(
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//创建列表项
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue,
child: new Text('SliverFillViewport'),
);
},
childCount: 1
),
viewportFraction: 1.0, // 占屏幕的比例
),
SliverFillRemaining
SliverFillRemaining 用于填充完剩余视图里面的全部空间, 详见 示例2
SliverSafeArea
SliverSafeArea 通过足够的填充来插入另一条条子以防止操作系统入侵的条子。例如,这将使条子缩进足以避开屏幕顶部的状态栏。为了防止各种边界的越界,比如说越过顶部的状态栏
示例1: 基础用法
import 'package:flutter/material.dart';
void main() {
runApp(new StartApp());
}
class StartApp extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new _StartAppState();
}
}
class _StartAppState extends State<StartApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '首页',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: CustomScrollViewTestRoute(),
);
}
}
class CustomScrollViewTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 因为本路由没有使用Scaffold,为了让子级Widget(如Text)使用
// Material Design 默认的样式风格,我们使用Material作为本路由的根。
return Material(
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
actions: <Widget>[
FlatButton(
child: Text('click me', style: TextStyle(
fontSize: 20,
color: Colors.white
),),
),
],
backgroundColor: Theme.of(context).accentColor,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.asset('assets/imgs/bg.jpg', fit: BoxFit.cover),
title: Text('Demo'),
),
pinned: true,
),
SliverFixedExtentList(
itemExtent: 120.0,
delegate: SliverChildListDelegate(
[1,2,3,4,5,6,7].map((product) {
return Container(
alignment: Alignment.center,
child: new Text('list item $product'),
);
}).toList(),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 180.0,
child: Container(
child: Image.asset(
"assets/imgs/bg.jpg", fit: BoxFit.fitWidth,),
),
),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: new SliverGrid( //Grid
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //Grid按两列显示
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//创建子widget
return new Container(
alignment: Alignment.center,
color: Colors.cyan[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
),
SliverToBoxAdapter(
child: Placeholder(fallbackHeight: 100,),
),
new SliverFixedExtentList(
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//创建列表项
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 50 //50个列表项
),
),
],
)
);
}
}
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 => maxHeight;
@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;
}
}
效果:
示例2: Sliver-sticky效果
class _StickyDemoState extends State<StickyDemo>
with SingleTickerProviderStateMixin {
TabController tabController;
@override
void initState() {
super.initState();
this.tabController = TabController(length: 2, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
elevation: 0,
expandedHeight: 250,
flexibleSpace: FlexibleSpaceBar(
title: Text('Sliver-sticky效果'),
background: Image.network(
'http://img1.mukewang.com/5c18cf540001ac8206000338.jpg',
fit: BoxFit.cover,
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: StickyTabBarDelegate(
child: TabBar(
labelColor: Colors.black,
controller: this.tabController,
tabs: <Widget>[
Tab(text: 'Home'),
Tab(text: 'Profile'),
],
),
),
),
SliverFillRemaining(
child: TabBarView(
controller: this.tabController,
children: <Widget>[
Center(child: Text('Content of Home')),
Center(child: Text('Content of Profile')),
],
),
),
],
),
);
}
}
class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
final TabBar child;
StickyTabBarDelegate({@required this.child});
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return this.child;
}
@override
double get maxExtent => this.child.preferredSize.height;
@override
double get minExtent => this.child.preferredSize.height;
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
效果: