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;@overridedouble get minExtent => minHeight;@overridedouble get maxExtent => maxHeight;@overrideWidget build(BuildContext context, double shrinkOffset, bool overlapsContent) {return new SizedBox.expand(child: child);}@overridebool 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 {@overrideState<StatefulWidget> createState() {return new _StartAppState();}}class _StartAppState extends State<StartApp> {@overrideWidget build(BuildContext context) {return MaterialApp(title: '首页',theme: new ThemeData(primarySwatch: Colors.blue,),home: CustomScrollViewTestRoute(),);}}class CustomScrollViewTestRoute extends StatelessWidget {@overrideWidget 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( //GridgridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, //Grid按两列显示mainAxisSpacing: 10.0,crossAxisSpacing: 10.0,childAspectRatio: 4.0,),delegate: new SliverChildBuilderDelegate((BuildContext context, int index) {//创建子widgetreturn 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;@overridedouble get minExtent => minHeight;@overridedouble get maxExtent => maxHeight;@overrideWidget build(BuildContext context, double shrinkOffset, bool overlapsContent) {return new SizedBox.expand(child: child);}@overridebool 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;@overridevoid initState() {super.initState();this.tabController = TabController(length: 2, vsync: this);}@overrideWidget 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});@overrideWidget build(BuildContext context, double shrinkOffset, bool overlapsContent) {return this.child;}@overridedouble get maxExtent => this.child.preferredSize.height;@overridedouble get minExtent => this.child.preferredSize.height;@overridebool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {return true;}}
效果:
