该文已授权公众号 「码个蛋」,转载请指明出处
前面的小节基本上讲完了常用的部件和容器部件,也可以完成很多的界面,但是又一个问题,假如我们要显示一段文字,比如将 一段又臭又长的文字
在界面上显示 1000 次,不难完成吧
// ..省略一些无关代码body: Text('一段又臭又长的文字' * 1000, softWrap: true)复制代码
很简单,运行到手机...「诶诶诶,**,怎么只显示了一部分,剩下的怎么画不下去」
日常开发中,会遇到很多这种情况,许多界面不是一页就能够显示完的。那么这里提下可滑动的容器部件
SingleChildScrollView
这个部件非常简单,不贴源码了。最简单的使用方式只需要提供一个 child
即可。现在给前面写的 Text
包裹上一层 SingleChildScrollView
然后再运行,文字全部都展示出来了。
如果需要实现一个垂直的滚动列表,可以直接通过 SingleChildScrollView
包裹 Column
来实现,列表内容全部塞到 Column
即可
class SingleChildScrollDemoPage extends StatelessWidget { @override Widget build(BuildContext context) { /// letters 自由发挥吧...一定要大量,大量,大量 Listletters = [......]; return Scaffold( appBar: AppBar( title: Text('Single Child Demo'), ), body: SingleChildScrollView( child: Center( child: Column( children: List.generate( letters.length, (index) => Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text(letters[index], style: TextStyle(fontSize: 18.0)), )), ), )), ); }}复制代码
运行结果会根据你的 letters
不同而不同,这边就不贴效果图了,反正你可以看到一串列表...
那么如果需要实现横向滚动列表呢,稍稍做下修改就行了
body: SingleChildScrollView( // 设置滚动方向 scrollDirection: Axis.horizontal, child: Center( // 修改为 `Row` 即可 child: Row( children: List.generate( letters.length, // 如果你的 letters 数量比较少,推荐加个 `Container` 把宽度指定大点 (index) => Container( child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0), child: Text(letters[index], style: TextStyle(fontSize: 18.0)), ), width: 30.0)), ), ))复制代码
效果图也不贴了,都比较简单。
该部分代码查看 single_child_scroll_main.dart
文件*
ListView
平时开发 Android
的时候,如果有相同格式的列表要实现,一般会使用 ListView
或者 RecyclerView
来实现,Flutter
也提供了类似的部件 ListView
实现 ListView
的方法主要有
- 通过
ListView
设置children
属性实现 - 通过
ListView.custom
实现 - 通过
ListView.builder
实现 - 通过
ListView.separated
实现带分割线列表
ListView children
第一种方法实现列表,和通过 SingleChildScrollView
+ Column
/ Row
的方法比较类似,不过可以直接通过指定 ListView
的 scrollDirection
就可以了。
body: ListView( // 通过修改滑动方向设置水平或者垂直方向滚动 scrollDirection: Axis.vertical, // 通过 iterable.map().toList 和 List.generate 方法效果是一样的 children: letters .map((s) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Center( child: Text(s)))) .toList()),复制代码
ListView.custom
body: ListView.custom( // 指定 item 的高度,可以加快渲染的速度 itemExtent: 40.0, // item 代理 childrenDelegate: SliverChildBuilderDelegate( // IndexedWidgetBuilder,根据 index 设置 item 中需要变化的数据 (_, index) => Center(child: Text(letters[index], style: TextStyle(color: Colors.red))), // 指定 item 的数量 childCount: letters.length, )),复制代码
如果每个 item
的高度可以确定,那么推荐通过 itemExtent
来设置 item
的高度/宽度,能够加快 ListView
的渲染速度。如果不指定高度/宽度,ListView
需要根据每个 item
来计算 ListView
的高度,这个计算过程是需要消耗时间和资源的
ListView.builder
该方法同 custom
类似,custom
需要通过一个 Delegate
生成 item
,该方法直接通过 builder
生成,同时也可以直接指定 item
的高度
body: ListView.builder( itemBuilder: (_, index) => Center(child: Text(letters[index], style: TextStyle(color: Colors.green))), itemExtent: 40.0, itemCount: letters.length),复制代码
相对比较简单,代码也比较少...就冲这点,我也愿意用这个方法
#####ListView.separated
如果需要在每个 item
之间添加分割线,那么通过以上的方式实现就比较困难了,所以 Flutter
提供了 separated
方法用来快速构建带有分割线的 ListView
加入我们的 item
之间的分割线需要如下样式:奇数位和偶数位之间用黑色分割线,偶数位和奇数位之间用红色分割线
// 需要分割线的时候才使用,不能指定 item 的高度body: ListView.separated( itemBuilder: (_, index) => Padding( padding: const EdgeInsets.symmetric(vertical: 20.0), child: Center(child: Text(letters[index], style: TextStyle(color: Colors.blue))), ), // 这里用来定义分割线 separatorBuilder: (_, index) => Divider(height: 1.0, color: index % 2 == 0 ? Colors.black : Colors.red), itemCount: letters.length),复制代码
最终的效果如下
以上代码查看 listview_main.dart
文件
总结下:如果 item
的高度能够准确获取,一定要指定 itemExtent
的值,这样会更加高效,至于要通过哪种方式来生成,完全看个人喜好吧。
ExpansionTile
既然讲到了 ListView
,在日常开发中,折叠列表也是一个比较常用的,所以这边要提下 ExpansionTile
这个部件,因为相对比较简单,所以直接上代码了
class ExpansionTilesDemoPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('ExpansionTile Demo'), ), body: ExpansionTile( // 最前面的 widget leading: Icon(Icons.phone_android), // 替换默认箭头// trailing: Icon(Icons.phone_iphone), title: Text('Parent'), // 默认是否展开 initiallyExpanded: true, // 展开时候的背景色 backgroundColor: Colors.yellow[100], // 展开或者收缩的回调,true 表示展开 onExpansionChanged: (expanded) => print('ExpansionTile is ${expanded ? 'expanded' : 'collapsed'}'), children: List.generate( 10, (position) => Container( padding: const EdgeInsets.only(left: 80.0), child: Text('Children ${position + 1}'), height: 50.0, alignment: Alignment.centerLeft, )), ), ); }}复制代码
这样就完成了一个折叠部件,看下最后的效果
那么实现折叠列表也就是通过 ListView
创建一个 ExpansionTile
列表即可,先准备下模拟的数据
final _keys = ['ParentA', 'ParentB', 'ParentC', 'ParentD', 'ParentE', 'ParentF']; final Map> _data = { 'ParentA': ['Child A0', 'Child A1', 'Child A2', 'Child A3', 'Child A4', 'Child A5'], 'ParentB': ['Child B0', 'Child B1', 'Child B2', 'Child B3', 'Child B4', 'Child B5'], 'ParentC': ['Child C0', 'Child C1', 'Child C2', 'Child C3', 'Child C4', 'Child C5'], 'ParentD': ['Child D0', 'Child D1', 'Child D2', 'Child D3', 'Child D4', 'Child D5'], 'ParentE': ['Child E0', 'Child E1', 'Child E2', 'Child E3', 'Child E4', 'Child E5'], 'ParentF': ['Child F0', 'Child F1', 'Child F2', 'Child F3', 'Child F4', 'Child F5'] };复制代码
在平时开发过程中,后台返回的数据应该是列表嵌套列表的形式比较多,我这边主要就是为了偷懒就随便弄了,接着修改下 body
的代码
body: ListView( children: _keys .map((key) => ExpansionTile( title: Text(key), children: _data[key] .map((value) => InkWell( child: Container( child: Text(value), padding: const EdgeInsets.only(left: 80.0), height: 50.0, alignment: Alignment.centerLeft, ), onTap: () {})) .toList(), )) .toList()),复制代码
最终的效果就是个折叠列表了
该部分代码查看 expansion_tile_main.dart
文件
当然了,只要数据到位,别说两层折叠,三层,四层甚至更多层都能够实现,源码中有实现四层的 demo
,这边就不贴代码了,有需要的小伙伴可以查看源码
GridView
生成列表可以通过 ListView
来实现,那么同样,实现网格列表 Flutter
也提供了 GridView
来实现,实现 GridView
的方法也很多...我数了下,大概有 10 种..对你没看错,就是那么多,(诶诶诶,别走啊...虽然方法有点多,但是,大同小异)
GridView
GridView
需要一个 gridDelegate
,gridDelegate
目前有两种
SliverGridDelegateWithFixedCrossAxisCount
看命名就知道,值固定数量的,这个数量是只单排的数量SliverGridDelegateWithMaxCrossAxisExtent
这个是设置最大宽度/高度,在这个值范围内取最大值,比如一排能给你排下 6 个,但是远不到设置的最大值,它绝不给你排 6 个
那么接下来的使用就比较简单了
class GridViewDemoPage extends StatelessWidget { // 自行设置 final Listletters = [ ..... ]; // 用于区分网格单元 final List colors = [Colors.red, Colors.green, Colors.blue, Colors.pink]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('GridView Demo'), ), body: GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, // 单行的个数 mainAxisSpacing: 10.0, // 同 scrollDirection 挂钩,item 之间在主轴方向的间隔 crossAxisSpacing: 10.0, // item 之间在副轴方法的间隔 childAspectRatio: 1.0 // item 的宽高比 ), // 需要根据 index 设置不同背景色,所以使用 List.generate,如果不设置背景色,也可用 iterable.map().toList children: List.generate( letters.length, (index) => Container( alignment: Alignment.center, child: Text(letters[index]), color: colors[index % 4], )), ), ); }}复制代码
关键地方已经添加了注释,跑下运行效果
接下来换一种 delegate
试试效果,当然这个最大值可以根据个人喜好来设置
body: GridView( // 通过设置 `maxCrossAxisExtent` 来指定最大的宽度,在这个值范围内,会选取相对较大的值 gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 60.0, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0), children: List.generate( letters.length, (index) => Container( alignment: Alignment.center, child: Text(letters[index]), color: colors[index % 4], )), )复制代码
最后效果:
为了方便写法呢,Flutter
对以上的两种方式进行了封装,省略了 delegate
GridView.count/GridView.extent
直接看下如何修改
// 这种情况简化了 `GridView` 使用 `SliverGridDelegateWithFixedCrossAxisCount` 代理的方法 body: GridView.count( crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0, crossAxisCount: 5, childAspectRatio: 2.0, children: List.generate( letters.length, (index) => Container( alignment: Alignment.center, color: colors[index % 4], child: Text(letters[index]), ))),复制代码
// 这种情况简化了 `GridView` 使用 `SliverGridDelegateWithMaxCrossAxisExtent` 代理的方法 body: GridView.extent( crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0, maxCrossAxisExtent: 60.0, children: List.generate( letters.length, (index) => Container( alignment: Alignment.center, color: colors[index % 4], child: Text(letters[index]), ))),复制代码
运行的效果入和前面的相同
GridView.custom
这种生成方式,比 GridView
多了一个 childrenDelegate
,childrenDelegate
主要分为两种,一种是通过 IndexedWidgetBuilder
来构建 item
的 SliverChildBuilderDelegate
,还有一种是通过 List
来构建 item
的 SliverChildListDelegate
,所以...这边直接有 4 中生成方式,当然,我们只需要了解 childrenDelegate
如何使用即可
body: GridView.custom( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 1.0), // item 通过 delegate 来生成,内部实现还是 `IndexedWidgetBuilder` childrenDelegate: SliverChildBuilderDelegate( (_, index) => Container( alignment: Alignment.center, color: colors[index % 4], child: Text(letters[index]), ), childCount: letters.length)),复制代码
body: GridView.custom( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 1.0), // 内部通过返回控件列表实现 childrenDelegate: SliverChildListDelegate( List.generate( letters.length, (index) => Container( child: Text(letters[index]), alignment: Alignment.center, color: colors[index % 4], )), )),复制代码
运行效果也同上面,不多帖了。
GridView.builder
前面介绍的方法中,生成 item
的方式基本上是通过 List
进行转换的,在 custom
提到了 IndexWidgetBuilder
的生成方式,当然,在 ListView
的时候也用到了这种生成方式,当然 GridView
也有啊,要「雨露均沾」你说是吧
// 通过 `IndexedWidgetBuilder` 来构建 item,别的参数同上 body: GridView.builder( // 这里又需要分两种 `gridDelegate` gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0), itemCount: letters.length, itemBuilder: (_, index) => Container(color: colors[index % 4], child: Text(letters[index]), alignment: Alignment.center)),复制代码
到这 10 种方式就说完了。终于可以歇一口气了。
该部分代码查看 gridview_main.dart
文件
CustomScrollView
在平时的开发中,应该会遇到这么种情况,头部是一个 GridView
接下来拼接一些别的部件,然后再拼接一个列表,例如下图
因为 GridView
和 ListView
亮着都是可滑动的部件,直接拼接肯定会有「滑动冲突」,所以 Flutter
就提供了一个粘合剂,CustomScrollView
,那么 Flutter
如何实现呢,因为会涉及到 Sliver
系列部件,所以这边先看下大概的代码,下节会补充 Sliver
系列部件的内容
class CustomScrollDemoPage extends StatelessWidget { // 这边用的 A-Z 字母 final Listletters = [ ..... ]; final List colors = [Colors.red, Colors.green, Colors.blue, Colors.pink, Colors.yellow, Colors.deepPurple]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('CustomScrollDemo'), ), body: CustomScrollView( // 这里需要传入 `Sliver` 部件,下节课填坑 slivers: [ // SliverGrid 实现同 GridView 实现方式一样 // 同样 SliverGrid 有提供 `count`, `entent` 方法便于快速生成 SliverGrid SliverGrid( delegate: SliverChildBuilderDelegate( (_, index) => InkWell( child: Image.asset('images/ali.jpg'), onTap: () {}, ), childCount: 8), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0)), // 这里下节讲 SliverToBoxAdapter( child: Container( color: Colors.black12, margin: const EdgeInsets.symmetric(vertical: 10.0), child: Column(children: [ Divider(height: 2.0, color: Colors.black54), Stack( alignment: Alignment.center, children: [ Image.asset('images/app_bar_hor.jpg', fit: BoxFit.cover), Text('我是一些别的东西..例如广告', textScaleFactor: 1.5, style: TextStyle(color: Colors.red)) ], ), Divider(height: 2.0, color: Colors.black54), ], mainAxisAlignment: MainAxisAlignment.spaceBetween), alignment: Alignment.center)), // SliverFixedExtentList 实现同 List.custom 实现类似 SliverFixedExtentList( delegate: SliverChildBuilderDelegate( (_, index) => InkWell( child: Container( child: Text(letters[index] * 10, style: TextStyle(color: colors[index % colors.length], letterSpacing: 2.0), textScaleFactor: 1.5), alignment: Alignment.center, ), onTap: () {}, ), childCount: letters.length), itemExtent: 60.0) ], ), ); }}复制代码
该部分代码查看 custom_scroll_main.dart
文件
滑动部件其实还有好几个,但是以上介绍的在平时开发过程中够用了,如果后期发现还需要别的部件,我会继续补上。在结束前,我们再说下如何通过 ScrollController
来控制 Scrollable
的滚动位置。例如我们需要实现,当滚动的距离大于一定距离的时候显示一个回到顶部的按钮,有了 ScrollController
就能够非常方便的实现
ScrollController
因为需要根据滑动的距离显示回到顶部按钮,那么就需要通过一个状态位来控制按钮显隐
class ScrollControllerDemoPage extends StatefulWidget { @override _ScrollControllerDemoPageState createState() => _ScrollControllerDemoPageState();}class _ScrollControllerDemoPageState extends State{ var _scrollController = ScrollController(); var _showBackTop = false; @override void initState() { super.initState(); // 对 scrollController 进行监听 _scrollController.addListener(() { // _scrollController.position.pixels 获取当前滚动部件滚动的距离 // window.physicalSize.height 获取屏幕高度 // 当滚动距离大于 800 后,显示回到顶部按钮 setState(() => _showBackTop = _scrollController.position.pixels >= 800); }); } @override void dispose() { // 记得销毁对象 _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('ScrollController Demo'), ), body: ListView( controller: _scrollController, children: List.generate( 20, (index) => Container(height: 50.0, alignment: Alignment.center, child: Text('Item ${index + 1}'))), ), floatingActionButton: _showBackTop // 当需要显示的时候展示按钮,不需要的时候隐藏,设置 null ? FloatingActionButton( onPressed: () { // scrollController 通过 animateTo 方法滚动到某个具体高度 // duration 表示动画的时长,curve 表示动画的运行方式,flutter 在 Curves 提供了许多方式 _scrollController.animateTo(0.0, duration: Duration(milliseconds: 500), curve: Curves.decelerate); }, child: Icon(Icons.vertical_align_top), ) : null, ); }}复制代码
最后的效果图
好啦,这节就到这,下节继续填这节课留下的坑。
最后代码的地址还是要的:
-
文章中涉及的代码:
-
基于郭神
cool weather
接口的一个项目,实现BLoC
模式,实现状态管理: -
一个课程(当时买了想看下代码规范的,代码更新会比较慢,虽然是跟着课上的一些写代码,但是还是做了自己的修改,很多地方看着不舒服,然后就改成自己的实现方式了):
如果对你有帮助的话,记得给个 Star,先谢过,你的认可就是支持我继续写下去的动力~