该文已授权公众号 「码个蛋」,转载请指明出处
前面的小节基本上讲完了常用的部件和容器部件,也可以完成很多的界面,但是又一个问题,假如我们要显示一段文字,比如将 一段又臭又长的文字
在界面上显示 1000 次,不难完成吧
// ..省略一些无关代码
body: Text('一段又臭又长的文字' * 1000, softWrap: true)
复制代码
很简单,运行到手机...「诶诶诶,**,怎么只显示了一部分,剩下的怎么画不下去」
日常开发中,会遇到很多这种情况,许多界面不是一页就能够显示完的。那么这里提下可滑动的容器部件
SingleChildScrollView
这个部件非常简单,不贴源码了。最简单的使用方式只需要提供一个 child
即可。现在给前面写的 Text
包裹上一层 SingleChildScrollView
然后再运行,文字全部都展示出来了。
如果需要实现一个垂直的滚动列表,可以直接通过 SingleChildScrollView
包裹 Column
来实现,列表内容全部塞到 Column
即可
class SingleChildScrollDemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
/// letters 自由发挥吧...一定要大量,大量,大量
List<String> letters = [......];
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<String, List<String>> _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 List<String> letters = [ ..... ];
// 用于区分网格单元
final List<Color> 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 List<String> letters = [ ..... ];
final List<Color> 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: <Widget>[
// 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: <Widget>[
Divider(height: 2.0, color: Colors.black54),
Stack(
alignment: Alignment.center,
children: <Widget>[
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<ScrollControllerDemoPage> {
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,
);
}
}
复制代码
最后的效果图
好啦,这节就到这,下节继续填这节课留下的坑。
最后代码的地址还是要的:
-
文章中涉及的代码:demos
-
基于郭神
cool weather
接口的一个项目,实现BLoC
模式,实现状态管理:flutter_weather -
一个课程(当时买了想看下代码规范的,代码更新会比较慢,虽然是跟着课上的一些写代码,但是还是做了自己的修改,很多地方看着不舒服,然后就改成自己的实现方式了):flutter_shop
如果对你有帮助的话,记得给个 Star,先谢过,你的认可就是支持我继续写下去的动力~