三. GridView组件
GridView用于展示多列的展示,在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。
在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。
3.1. GridView构造函数
我们先学习GridView构造函数的使用方法
一种使用GridView的方式就是使用构造函数来创建,和ListView对比有一个特殊的参数:gridDelegate
gridDelegate
用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount({ @requireddouble crossAxisCount, // 交叉轴的item个数 double mainAxisSpacing = 0.0, // 主轴的间距 double crossAxisSpacing = 0.0, // 交叉轴的间距 double childAspectRatio = 1.0, // 子Widget的宽高比 })
代码演练:
class MyGridCountDemo extends StatelessWidget { List<Widget> getGridWidgets() { returnList.generate(100, (index) { return Container( color: Colors.purple, alignment: Alignment(0, 0), child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)), ); }); } @override Widget build(BuildContext context) { return GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.0 ), children: getGridWidgets(), ); } }
SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithMaxCrossAxisExtent({ double maxCrossAxisExtent, // 交叉轴的item宽度 double mainAxisSpacing = 0.0, // 主轴的间距 double crossAxisSpacing = 0.0, // 交叉轴的间距 double childAspectRatio = 1.0, // 子Widget的宽高比 })
代码演练:
class MyGridExtentDemo extends StatelessWidget { List<Widget> getGridWidgets() { returnList.generate(100, (index) { return Container( color: Colors.purple, alignment: Alignment(0, 0), child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)), ); }); } @override Widget build(BuildContext context) { return GridView( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 150, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.0 ), children: getGridWidgets(), ); } }
前面两种方式也可以不设置delegate
可以分别使用:GridView.count构造函数
和GridView.extent
构造函数实现相同的效果,这里不再赘述。
3.2. GridView.build
和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build
来交给GridView自己管理需要创建的子Widget。
我们直接使用之前的数据来进行代码演练:
class _GridViewBuildDemoState extends State<GridViewBuildDemo> { List<Anchor> anchors = []; @override void initState() { getAnchors().then((anchors) { setState(() { this.anchors = anchors; }); }); super.initState(); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), child: GridView.builder( shrinkWrap: true, physics: ClampingScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.2 ), itemCount: anchors.length, itemBuilder: (BuildContext context, int index) { return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Image.network(anchors[index].imageUrl), SizedBox(height: 5), Text(anchors[index].nickname, style: TextStyle(fontSize: 16),), Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,) ], ), ); } ), ); } }
四. Slivers
我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。
补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。
4.1. Slivers的基本使用
因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:
- SliverList:类似于我们之前使用过的ListView;
- SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;
- SliverGrid:类似于我们之前使用过的GridView;
- SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
- SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
- SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)
我们简单演示一下:SliverGrid+SliverPadding+SliverSafeArea的组合
class HomeContent extends StatelessWidget { @override Widget build(BuildContext context) { return CustomScrollView( slivers: <Widget>[ SliverSafeArea( sliver: SliverPadding( padding: EdgeInsets.all(8), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment(0, 0), color: Colors.orange, child: Text("item$index"), ); }, childCount: 20 ), ), ), ) ], ); } }
4.2. Slivers的组合使用
这里我使用官方的示例程序,将SliverAppBar+SliverGrid+SliverFixedExtentList做出如下界面:
class HomeContent extends StatelessWidget { @override Widget build(BuildContext context) { return showCustomScrollView(); } Widget showCustomScrollView() { returnnew CustomScrollView( slivers: <Widget>[ const SliverAppBar( expandedHeight: 250.0, flexibleSpace: FlexibleSpaceBar( title: Text('Coderwhy Demo'), background: Image( image: NetworkImage( "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg", ), fit: BoxFit.cover, ), ), ), new SliverGrid( gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200.0, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 4.0, ), delegate: new SliverChildBuilderDelegate( (BuildContext context, int index) { returnnew Container( alignment: Alignment.center, color: Colors.teal[100 * (index % 9)], child: new Text('grid item $index'), ); }, childCount: 10, ), ), SliverFixedExtentList( itemExtent: 50.0, delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { returnnew Container( alignment: Alignment.center, color: Colors.lightBlue[100 * (index % 9)], child: new Text('list item $index'), ); }, childCount: 20 ), ), ], ); } }
五. 监听滚动事件
对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
比如视图滚动到底部时,我们可能希望做上拉加载更多;
比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
比如监听滚动什么时候开始,什么时候结束;
在Flutter中监听滚动相关的内容由两部分组成:ScrollController和ScrollNotification。
5.1. ScrollController
在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
我们来做一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮:
jumpTo(double offset)
、animateTo(double offset,...)
:这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。- ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
class MyHomePage extends StatefulWidget { @override State<StatefulWidget> createState() => MyHomePageState(); } class MyHomePageState extends State<MyHomePage> { ScrollController _controller; bool _isShowTop = false; @override void initState() { // 初始化ScrollController _controller = ScrollController(); // 监听滚动 _controller.addListener(() { var tempSsShowTop = _controller.offset >= 1000; if (tempSsShowTop != _isShowTop) { setState(() { _isShowTop = tempSsShowTop; }); } }); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("ListView展示"), ), body: ListView.builder( itemCount: 100, itemExtent: 60, controller: _controller, itemBuilder: (BuildContext context, int index) { return ListTile(title: Text("item$index")); } ), floatingActionButton: !_isShowTop ? null : FloatingActionButton( child: Icon(Icons.arrow_upward), onPressed: () { _controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease); }, ), ); } }
5.2. NotificationListener
如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener
。
- NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
- NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
- 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为
true
时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false
时,则冒泡继续。
案例: 列表滚动, 并且在中间显示滚动进度
class MyHomeNotificationDemo extends StatefulWidget { @override State<StatefulWidget> createState() => MyHomeNotificationDemoState(); } class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> { int _progress = 0; @override Widget build(BuildContext context) { return NotificationListener( onNotification: (ScrollNotification notification) { // 1.判断监听事件的类型 if (notification is ScrollStartNotification) { print("开始滚动....."); } elseif (notification is ScrollUpdateNotification) { // 当前滚动的位置和总长度 final currentPixel = notification.metrics.pixels; final totalPixel = notification.metrics.maxScrollExtent; double progress = currentPixel / totalPixel; setState(() { _progress = (progress * 100).toInt(); }); print("正在滚动:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}"); } elseif (notification is ScrollEndNotification) { print("结束滚动...."); } returnfalse; }, child: Stack( alignment: Alignment(.9, .9), children: <Widget>[ ListView.builder( itemCount: 100, itemExtent: 60, itemBuilder: (BuildContext context, int index) { return ListTile(title: Text("item$index")); } ), CircleAvatar( radius: 30, child: Text("$_progress%"), backgroundColor: Colors.black54, ) ], ), ); } }