1 ListView

ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。

(1) 构造函数

一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。

  1. class MyHomeBody extends StatelessWidget {
  2. MyHomeBody({Key? key}) : super(key: key);
  3. final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
  4. @override
  5. Widget build(BuildContext context) {
  6. return Scaffold(
  7. body: ListView(
  8. children: [
  9. ListTile(
  10. leading: Icon(Icons.people, size: 36,),
  11. title: Text("联系人"),
  12. subtitle: Text("联系人信息"),
  13. trailing: Icon(Icons.arrow_forward_ios),
  14. ),
  15. ListTile(
  16. leading: Icon(Icons.email, size: 36,),
  17. title: Text("邮箱"),
  18. subtitle: Text("邮箱地址信息"),
  19. trailing: Icon(Icons.arrow_forward_ios),
  20. ),
  21. ListTile(
  22. leading: Icon(Icons.message, size: 36,),
  23. title: Text("消息"),
  24. subtitle: Text("消息详情信息"),
  25. trailing: Icon(Icons.arrow_forward_ios),
  26. ),
  27. ListTile(
  28. leading: Icon(Icons.map, size: 36,),
  29. title: Text("地址"),
  30. subtitle: Text("地址详情信息"),
  31. trailing: Icon(Icons.arrow_forward_ios),
  32. )
  33. ],
  34. ),
  35. );
  36. }
  37. }

image.png

(2) ListView.build

通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。
但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。
我们可以ListView.build来构建子Widget,提供性能。
该构造函数将创建子Widget交给了一个抽象的方法,交给ListView进行管理,ListView会在真正需要的时候去创建子Widget,而不是一开始就全部初始化好。
该方法有两个重要参数:

  • itemBuilder:列表项创建的方法。当列表滚动到对应位置的时候,ListView会自动调用该方法来创建对应的子Widget。类型是IndexedWidgetBuilder,是一个函数类型。
  • itemCount:表示列表项的数量,如果为空,则表示ListView为无限列表。
    1. class MyHomeBody extends StatelessWidget {
    2. @override
    3. Widget build(BuildContext context) {
    4. return ListView.builder(
    5. itemCount: 100,
    6. itemExtent: 80,
    7. itemBuilder: (BuildContext context, int index) {
    8. return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
    9. }
    10. );
    11. }
    12. }

    (3) 加载json数据

    我们现在动态的来通过JSON数据展示一个列表, 当前我们的数据是异步加载的,刚开始界面并不会展示数据(没有数据),后面从JSON中加载出来数据(有数据)后,再次展示加载的数据。这个时候,我们需要使用StatefulWidget来管理组件。 ```dart class _MyHomeBodyState extends State { List anchors = []; // 在初始化状态的方法中加载数据 @override void initState() { getAnchors().then((anchors) {
    1. setState(() {
    2. this.anchors = anchors;
    3. });
    }); super.initState(); }

@override Widget build(BuildContext context) { return Scaffold( body: ListView.builder( itemBuilder: (BuildContext context, int index) { return Padding( padding: EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Image.network( anchors[index].imageUrl, fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), SizedBox(height: 8), Text(anchors[index].nickname, style: TextStyle(fontSize: 20),), SizedBox(height: 5), Text(anchors[index].roomName) ], ), ); }, ), ); } }

class Anchor { late String nickname; late String roomName; late String imageUrl;

Anchor({ required this.nickname, required this.roomName, required this.imageUrl });

Anchor.withMap(Map parsedMap) { this.nickname = parsedMap[“nickname”]; this.roomName = parsedMap[“roomName”]; this.imageUrl = parsedMap[“roomSrc”]; } }

Future> getAnchors() async { // 1.读取json文件 String jsonString = await rootBundle.loadString(“assets/yz.json”);

// 2.转成List或Map类型 final jsonResult = json.decode(jsonString);

// 3.遍历List,并且转成Anchor对象放到另一个List中 List anchors = []; for (Map map in jsonResult) { anchors.add(Anchor.withMap(map)); } return anchors; }

  1. <a name="qEPS1"></a>
  2. ## (4) 滚动方向scrollDirection
  3. ```dart
  4. class _MyHomeBodyState extends State<MyHomeBody> {
  5. @override
  6. Widget build(BuildContext context) {
  7. return Scaffold(
  8. body: ListView(
  9. scrollDirection: Axis.horizontal,
  10. children: <Widget>[
  11. Container(color: Colors.red, width: 20),
  12. Container(color: Colors.green, width: 30),
  13. Container(color: Colors.blue, width: 40),
  14. Container(color: Colors.purple, width: 40),
  15. Container(color: Colors.orange, width: 50),
  16. ],
  17. )
  18. );
  19. }
  20. }

image.png

(5) ListView.separated

ListView.separated可以生成列表项之间的分割器,它除了比ListView.builder多了一个separatorBuilder参数,该参数是一个分割器生成器。

  1. class _MyHomeBodyState extends State<MyHomeBody> {
  2. @override
  3. Widget build(BuildContext context) {
  4. Divider blueColor = Divider(color: Colors.blue);
  5. Divider redColor = Divider(color: Colors.red);
  6. return Scaffold(
  7. body: ListView.separated(
  8. itemBuilder: (BuildContext context, int index) {
  9. return ListTile(
  10. leading: Icon(Icons.people),
  11. title: Text("联系人${index + 1}"),
  12. subtitle: Text("联系人电话${index + 1}"),
  13. );
  14. },
  15. separatorBuilder: (BuildContext context, int index) {
  16. return index % 2 == 0 ? redColor : blueColor;
  17. },
  18. itemCount: 100));
  19. }
  20. }

image.png

2 GridView

(1) 构建函数

gridDelegate用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:SliverGridDelegateWithFixedCrossAxisCount

  1. SliverGridDelegateWithFixedCrossAxisCount({
  2. @requireddouble crossAxisCount, // 交叉轴的item个数
  3. double mainAxisSpacing = 0.0, // 主轴的间距
  4. double crossAxisSpacing = 0.0, // 交叉轴的间距
  5. double childAspectRatio = 1.0, // 子Widget的宽高比
  6. })
  1. class _MyHomeBodyState extends State<MyHomeBody> {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. body: GridView(
  6. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  7. crossAxisCount: 3,
  8. mainAxisSpacing: 10,
  9. crossAxisSpacing: 10,
  10. childAspectRatio: 1.0),
  11. children: getGridWidgets(),
  12. ));
  13. }
  14. List<Widget> getGridWidgets() {
  15. return List.generate(100, (index) {
  16. return Container(
  17. color: Colors.purple,
  18. alignment: Alignment(0, 0),
  19. child: Text("item$index",
  20. style: TextStyle(fontSize: 20, color: Colors.white)),
  21. );
  22. });
  23. }
  24. }

image.png

(2) GridView.build

和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget。

3 CustomScrollView

我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。

因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:

  • SliverList:类似于我们之前使用过的ListView;
  • SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;
  • SliverGrid:类似于我们之前使用过的GridView;
  • SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
  • SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
  • SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)

    (1) 构建函数

    1. class _MyHomeBodyState extends State<MyHomeBody> {
    2. @override
    3. Widget build(BuildContext context) {
    4. return Scaffold(
    5. body: CustomScrollView(
    6. slivers: <Widget>[
    7. SliverGrid(
    8. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    9. crossAxisCount: 2,
    10. crossAxisSpacing: 8,
    11. mainAxisSpacing: 8,
    12. childAspectRatio: 1.5,
    13. ),
    14. delegate: SliverChildBuilderDelegate(
    15. (BuildContext context, int index) {
    16. return Container(
    17. alignment: Alignment(0, 0),
    18. color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
    19. child: Text("item$index"),
    20. );
    21. },
    22. childCount: 5)),
    23. SliverFixedExtentList(
    24. itemExtent: 50.0,
    25. delegate: SliverChildBuilderDelegate(
    26. (BuildContext context, int index) {
    27. return Container(
    28. alignment: Alignment.center,
    29. color: Colors.lightBlue[100 * (index % 9)],
    30. child: new Text('list item $index'),
    31. );
    32. },
    33. childCount: 5
    34. ),
    35. ),
    36. ],
    37. ),
    38. );
    39. }
    40. }
    image.png

    4 监听滚动事件

    对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
    比如视图滚动到顶部时,我们可能希望做上拉加载更多;
    比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
    在Flutter中监听滚动相关的内容由两部分组成:ScrollController和ScrollNotification。

    (1) ScrollController

    在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
    ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
    通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
    我们来做一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮:
    jumpTo(double offset)、animateTo(double offset,…):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
    ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。 ```dart

class _MyHomeBodyState extends State { final ScrollController _controller = ScrollController(initialScrollOffset: 300); bool _isShowTop = false;

@override void initState() { super.initState(); _controller.addListener(() { setState(() { _isShowTop = _controller.offset >= 1000; }); }); }

@override Widget build(BuildContext context) { return Scaffold( body: ListView.builder( itemCount: 100, itemExtent: 60, controller: _controller, itemBuilder: (BuildContext context, int index) { return ListTile(title: Text(“item$index”)); }), floatingActionButton: _isShowTop ? FloatingActionButton( child: Icon(Icons.arrow_upward), onPressed: () { _controller.animateTo(0, duration: Duration(milliseconds: 100), curve: Curves.ease); }, ) : null, ); } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1114755/1648359052320-b387c708-5c82-45cd-8d89-f5252bc20de3.png#clientId=u091251d1-9110-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=286&id=u18650330&margin=%5Bobject%20Object%5D&name=image.png&originHeight=572&originWidth=874&originalType=binary&ratio=1&rotation=0&showTitle=false&size=43285&status=done&style=none&taskId=u88cfe933-35c2-4a3d-98a0-06943d96931&title=&width=437)
  2. <a name="eA9Ct"></a>
  3. ## (2) NotificationListener
  4. 如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener
  5. - NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
  6. - NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
  7. - 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
  8. ```dart
  9. class _MyHomeBodyState extends State<MyHomeBody> {
  10. int _progress = 0;
  11. @override
  12. Widget build(BuildContext context) {
  13. return Scaffold(
  14. body: NotificationListener(
  15. onNotification: (ScrollNotification notification) {
  16. // 1.判断监听事件的类型
  17. if (notification is ScrollStartNotification) {
  18. print("开始滚动.....");
  19. } else if (notification is ScrollUpdateNotification) {
  20. // 当前滚动的位置和总长度
  21. final currentPixel = notification.metrics.pixels;
  22. final totalPixel = notification.metrics.maxScrollExtent;
  23. double progress = currentPixel / totalPixel;
  24. setState(() {
  25. _progress = (progress * 100).toInt();
  26. });
  27. print("正在滚动:${notification.metrics.pixels} / ${notification.metrics.maxScrollExtent}");
  28. } else if (notification is ScrollEndNotification) {
  29. print("结束滚动....");
  30. }
  31. return false;
  32. },
  33. child: Stack(
  34. alignment: Alignment(.9, .9),
  35. children: <Widget>[
  36. ListView.builder(
  37. itemCount: 100,
  38. itemExtent: 60,
  39. itemBuilder: (BuildContext context, int index) {
  40. return ListTile(title: Text("item$index"));
  41. }
  42. ),
  43. CircleAvatar(
  44. radius: 30,
  45. child: Text("$_progress%"),
  46. backgroundColor: Colors.black54,
  47. )
  48. ],
  49. ),
  50. )
  51. );
  52. }
  53. }