首先了解几个widget
SliverAppBar
Flutter中的Slivers大家族基本都是配合CustomScrollView来实现的,除了上面提到的滑动布局嵌套,你还可以使用Slivers来实现页面头部展开/收起、 AppBar随手势变换等等功能。
给SliverAppBar设置flexibleSpace(展开后的内容)和expandedHeight(展开高度)属性,就可以轻松完成AppBar展开/收起的功能
- floating
如果设置floating属性为true,那么AppBar会在你做出下拉手势时就立即展开(即使ListView并没有到达顶部),该展开状态不显示flexibleSpace:
- snap
如果同时设置floating和snap属性为true,那么AppBar会在你做出下拉手势时就立即全部展开(即使ListView并没有到达顶部),该展开状态显示flexibleSpace:
- pinned
如果不想上滑到顶时AppBar消失,则设置pinned属性为true即可:
- preferredSize
只要你的控件实现了 preferredSize,就可以放到 AppBar 的 bottom 中使用。比如下图搜索栏,这是TabView下的页面又实用了AppBar。
SliverList
SliverChildListDelegate一般用来构item建数量明确的列表,会提前build好所有的子item,所以在效率上会有问题,适合item数量不多的情况(不超过一屏)。
SliverChildBuilderDelegate构建的列表理论上是可以无限长的,因为使用来lazily construct优化。
(两者的区别有些类似于ListView和ListView.builder()的区别。)
new SliverAppBar( //头部为一个SliverAppBar,折叠部分的内容都放在了flexibleSpace中。
pinned: true,
expandedHeight: 300.0,
// 这个高度必须比flexibleSpace高度大
forceElevated: innerBoxIsScrolled,
bottom: PreferredSize(
child: new Container(
child: new TabBar(
tabs: tabs,
labelColor: Colors.green,
unselectedLabelColor: Colors.grey,),
color: Colors.white,
),
preferredSize: new Size(double.infinity, 46.0)
),
flexibleSpace: new Container(
child: new Column(
children: <Widget>[
new AppBar(
title: Text("首页"),
centerTitle: true,
),
new Expanded(
child: new Container(
child: Image.asset(
"images/temp2.jpg",
repeat: ImageRepeat.repeat,
),
width: double.infinity,
),
)
],
),
),
),
SliverPersistentHeader
SliverPersistentHeader顾名思义,就是给一个可滑动的视图添加一个头(实际上,在CustomScrollView的slivers列表中,header可以出现在视图的任意位置,不一定要是在顶部)。这个Header会随着滑动而展开/收起,使用pinned和floating属性来控制收起时Header是否展示(pinned和floating属性不可以同时为true),pinned和floating属性的具体意义和SliverAppBar中相同,这里就不再次解释了。
写一个自定义SliverPersistentHeaderDelegate很简单,只需重写build()、get maxExtent、get minExtent和shouldRebuild()这四个方法,上面就是一个最简单的SliverPersistentHeaderDelegate的实现。其中,maxExtent表示header完全展开时的高度,minExtent表示header在收起时的最小高度。因此,对于我们上面的那个自定义Delegate,如果将minHeight和maxHeight的值设置为相同时,header就不会收缩了,这样的Header跟我们平常理解的Header更像。
例子:
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 => max(maxHeight, minHeight);
@override
Widget build(BuildContext context, double shrinkOffset,
bool overlapsContent) {
return child;
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
SliverToBoxAdapter
SliverPersistentHeader一般来说都是会展开/收起的(除非minExtent和maxExtent值相同),那么如果想要在滚动视图中添加一个普通的控件,那么就可以使用SliverToBoxAdapter来将各种视图组合在一起,放在CustomListView中。
看下我实现的效果:
代码:
import 'package:flutter/material.dart';
import 'package:mou/navigationbar/item/HomeGridItem.dart';
import 'package:mou/navigationbar/item/HomeItem.dart';
import 'package:mou/toast/Toast.dart';
import 'dart:math';
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new HomeState();
}
}
class HomeState extends State<HomePage> {
List<HomeItem> widgets = [];
List<HomeGridItem> Gridwidgets = [];
@override
Widget build(BuildContext context) {
return new DefaultTabController(
length: 2,
child: new Scaffold(
body: new CustomScrollView(
slivers: <Widget>[
new SliverAppBar( //头部为一个SliverAppBar,折叠部分的内容都放在了flexibleSpace中。
pinned: true,
// floating: true,//那么AppBar会在你做出下拉手势时就立即展开
expandedHeight: 220.0,
// 这个高度必须比flexibleSpace高度大
// forceElevated: innerBoxIsScrolled,
bottom: PreferredSize(
child: new Container(
width: 500,
height: 46,
color: Colors.white,
padding: EdgeInsets.only(left: 15.0, right: 15.0,top: 5.0, bottom: 5.0),
child: new Material(
color: Colors.white,
borderRadius: BorderRadius.circular(20.0),
elevation: 5.0,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.search),
new Text('点击搜索',
textAlign: TextAlign.center,
style: new TextStyle(
color: Colors.grey,
fontSize: 16
)
),
],
),
)
),
//只要你的控件实现了 preferredSize,就可以放到 AppBar 的 bottom 中使用。比如下图搜索栏,这是TabView下的页面又实用了AppBar。
preferredSize: new Size(double.infinity, 46.0)
),
flexibleSpace: new Container(
child: new Column(
children: <Widget>[
new AppBar(
title: Text("首页"),
centerTitle: true,
),
new Expanded(
child: new Container(
child: Image.asset(
"images/temp2.jpg",
fit: BoxFit.cover,
),
width: double.infinity,
),
)
],
),
),
),
// SliverPersistentHeader(
pinned: true,
floating: true,
// delegate: _SliverAppBarDelegate(
// minHeight: 50.0,
// maxHeight: 50.0,
// child: new Container(
// width: 500,
// margin: EdgeInsets.only(
// left: 15.0, right: 15.0, top: 10.0),
// child: new Material(
// color: Colors.white,
// borderRadius: BorderRadius.circular(20.0),
// elevation: 5.0,
// child: new Center(
// child: new Text('点击搜索',
// textAlign: TextAlign.center,
// style: new TextStyle(
// color: Colors.grey,
// fontSize: 18
// )
// ),
// ),
// )
// )
// ),
// ),
new SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context,
int index) {
return getItem(index);
},
childCount: widgets.length,
),
),
new SliverToBoxAdapter(
child: new Container(
margin: EdgeInsets.only(top: 15.0,),
child: new Row(
children: <Widget>[
new Expanded(child: new Container(
child: new Text("新闻内容1",
style: new TextStyle(
color: Colors.black45
),
),
padding: EdgeInsets.only(left: 20.0, top: 20.0),
margin: EdgeInsets.only(left: 15.0, right: 5.0),
height: 150,
decoration: new BoxDecoration(
borderRadius: BorderRadius.all(
const Radius.circular(5.0)),
image: DecorationImage(
image: AssetImage("images/temp3.jpg"),
fit: BoxFit.cover)
),
)
),
new Expanded(child: new Column(
children: <Widget>[
new Container(
child: new Text("新闻内容2",
style: new TextStyle(
color: Colors.black45
),
),
height: 70,
padding: EdgeInsets.only(left: 10.0, top: 10.0),
margin: EdgeInsets.only(
left: 5.0,
right: 15.0,
bottom: 5.0,),
width: double.infinity,
decoration: new BoxDecoration(
borderRadius: BorderRadius.all(
const Radius.circular(5.0)),
image: DecorationImage(
image: AssetImage("images/temp3.jpg"),
fit: BoxFit.fill)
),
),
new Container(
padding: EdgeInsets.only(left: 10.0, top: 10.0),
margin: EdgeInsets.only(
left: 5.0,
right: 15,
top: 5.0),
child: new Text("新闻内容3",
style: new TextStyle(
color: Colors.black45
),),
height: 70,
width: double.infinity,
decoration: new BoxDecoration(
borderRadius: BorderRadius.all(
const Radius.circular(5.0)),
image: DecorationImage(
image: AssetImage("images/temp3.jpg"),
fit: BoxFit.fill)
),
)
],
))
],
)
),
),
new SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context,
int index) {
return getItem(index);
},
childCount: widgets.length,
),
),
SliverPersistentHeader(
// pinned: true,
// floating: true,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 180.0,
child: new Card(
margin: EdgeInsets.only(left: 10.0, right: 10.0),
elevation: 5.0,
child: new Container(
padding: EdgeInsets.only(bottom: 20.0),
alignment: Alignment.bottomCenter,
decoration: new BoxDecoration(
image: DecorationImage(image: NetworkImage(
"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1134704593,3837890513&fm=26&gp=0.jpg"),
fit: BoxFit.cover,
)
),
child: new Text("这是中间一个条目",
style: new TextStyle(
color: Colors.white,
fontSize: 26,
),
)
),
)
),
),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //一行多少个
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return getGridItem(index);
},
childCount: Gridwidgets.length,
),
),
]
),
)
);
}
@override
void initState() {
super.initState();
for (var i = 0; i < 2; i++) {
widgets.add(HomeItem(
title: "大学生活",
position: i,
content: "啊啊啊啊啊啊啊啊啊啊啊啊啊",
time: "2018-10-2",
coverUrl: "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3155928136,23109883&fm=27&gp=0.jpg"));
}
for (var i = 0; i < 5; i++) {
Gridwidgets.add(HomeGridItem(
title: "大学生活",
position: i,
content: "啊啊啊啊啊啊啊啊啊啊啊啊啊",
time: "2018-10-2",
coverUrl: "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3155928136,23109883&fm=27&gp=0.jpg"));
}
}
Widget getItem(int i) {
return new HomeItem(
title: widgets[i].title,
position: widgets[i].position,
content: widgets[i].content,
time: widgets[i].time,
coverUrl: widgets[i].coverUrl,
callBack: (postition, item) {
Toast.toast(context, "position$postition");
},
);
}
Widget getGridItem(int i) {
return new HomeGridItem(
title: Gridwidgets[i].title,
position: Gridwidgets[i].position,
content: Gridwidgets[i].content,
time: Gridwidgets[i].time,
coverUrl: Gridwidgets[i].coverUrl,
callBack: (postition, item) {
Toast.toast(context, "position$postition");
},
);
}
}
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 => max(maxHeight, minHeight);
@override
Widget build(BuildContext context, double shrinkOffset,
bool overlapsContent) {
return child;
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
import 'package:flutter/material.dart';
import 'package:mou/toast/Toast.dart';
class HomeGridItem extends StatelessWidget {
HomeGridItem({this.title,
this.position,
this.content,
this.time,
this.coverUrl,
this.callBack,})
: super(key: new ObjectKey(position)); //ObjectKey唯一的标识位
String title;
int position;
String content;
String coverUrl;
String time;
CallBack callBack;
@override
Widget build(BuildContext context) {
print('bookId:$position');
Widget titleSection = new Container(
padding: const EdgeInsets.fromLTRB(10.0, 8.0, 10.0, 8.0),
child:
new Card(
elevation: 5.0,
child:new Container(
padding: const EdgeInsets.all(10.0),
child: new Column(
children: <Widget>[
new Container(
width: double.infinity,
height: 100.0,
margin: EdgeInsets.fromLTRB(0, 0, 0, 10.0),
child: new Image.network(coverUrl,
fit: BoxFit.cover,
),
),
new Text(
title,
style: new TextStyle(
fontSize: 18,
color: Color(0xFF333333)
),
),
],
),
)
)
);
return new GestureDetector(
child: titleSection,
onTap: () {
callBack(position, this);
},
);
}
}
typedef void CallBack(int position,
HomeGridItem item,);