一、说明

ListView 是 Flutter App 中使用非常多的滚动列表组件,页面大多数情况都是可滚动的,列表聚合图文场景下使用 ListView 的情况非常多。

ListView 除了本身这个 Widget 之外,一般会和 ListTile 配合使用,其中 ListTile 是使用非常多的子项 Widget

基本的结构就是:

|- ListView
  |- ListTile
  |- ListTile
  |- ListTile  

下面代码实践中使用了几个静态变量,这里列出来,完整代码可以在文章最后看到

页面结构的相关代码不重复,只写 ListView 相关的代码

const TITLE = '标题标题标题标题标题标题标题';
const SUB_TITLE = '二级标题二级标题二级标题二级标题二级标题二级标题二级标题二级标题二';
const IMAGE_SRC =
    'https://cdn.pixabay.com/photo/2019/05/20/13/22/portugal-4216645_1280.jpg';

二、ListView 基本用法

基本结构代码如下,各个 ListTile 子项也抽成了 Widget,直接在 ListView 的 children 参数中引入使用:

class ListViewDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        ListItemIcon(),
        ListItemIcon(),
        ListItemImage(),
        ListItemImage(),
        ListItemImage2(),
        ListItemImage2(),
        ListItemOnlyImage(),
        ListItemOnlyImage(),
      ],
      padding: EdgeInsets.all(12),
    );
  }
}

1、ListTile 标题 + Icon 用法

ListTile 支持的参数非常多,常用是 onTapleadingtrailingtitlesubtitle

一个 ListTile 也由 leadingtrailingtitle + subtitle 组成

return ListTile(
  trailing: Icon(Icons.chevron_right, color: Colors.pink),
  title: Text(TITLE),
  subtitle: Text(
    SUB_TITLE,
    maxLines: 1,
    overflow: TextOverflow.ellipsis,
  ),
);

上面代码中,创建了一个 ListTile,并且指定了 title 和 subtitle,title 和 subtitle 都是 Widget 类型,可以放 Widget,并不限制是 Text

trailing 可以放 Icon 也可以放别的 Widget

最终效果如下:

55379-11a7xi3m4hw.png

2、ListTile 图片+标题+Icon 用法

上面只是单纯的 标题+Icon,好一点的新闻列表效果会加上封面图,而封面图是放在 ListTileleading 属性中的

class ListItemImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Container(
          child: Image.network(
            IMAGE_SRC,
            fit: BoxFit.cover,
          ),
          color: Colors.grey,
          width: 60,
          height: 60),
      trailing: Icon(Icons.chevron_right, color: Colors.pink),
      title: Text(TITLE),
      subtitle: Text(
        SUB_TITLE,
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
      ),
    );
  }
}

上面代码中。leading 放了一张网络图片,网络加载图片之前也说过这个问题,就是太突兀,所以我做了一个 Container 放在外面,然后通过 Container Widget 限制图片宽高,并且设置了一个背景色,这样加载的时候不会显得过于突兀,其他的和上面代码一致。

因为我没有设置 title 只有一行,所以看起来并没有非常水平对齐

最终效果:

1.gif

3、ListTile 标题+图片 用法

上面是图片放在前面,而有些场景下,图片是放在 List 单个内容的后面,这种情况下,将图片放在 trailing 中即可:

return ListTile(
  trailing: Container(
      child: Image.network(
        IMAGE_SRC,
        fit: BoxFit.cover,
      ),
      color: Colors.grey,
      width: 60,
      height: 60),
  title: Text(TITLE),
  subtitle: Text(
    SUB_TITLE,
    maxLines: 1,
    overflow: TextOverflow.ellipsis,
  ),
);

最终效果:

73949-a4o1bbcpa74.png

4、不使用 ListTile 的列表

有些时候我们只是想构建一个列表,但是列表子项并不是 ListTile 的样式,这就需要我们自己实现一个 子项 Widget

而这种情况也非常简单,无非就是自己构造一个 Widget 然后引入使用即可

class ListItemOnlyImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Image.network(IMAGE_SRC, fit: BoxFit.cover),
      color: Colors.grey,
      width: 600,
      height: 400,
      margin: EdgeInsets.only(bottom: 10),
    );
  }
}

效果:

2.gif

三、基于列表数据渲染 ListView

上面都是我们写死的列表,实际过程中我们肯定是需要基于一个列表数据去渲染整个 List 的

列表数据可以通过接口请求拿到,这里我就直接用 网易新闻的结构 mock 了,数据列表可以在 https://github.com/postbird/FlutterHelloWorldDemo/blob/master/demo1/lib/mock/list.dart 拿到

我们也可以发现的是,ListView 直接的话 children 参数需要传递一个 List<Widget> 结构的列表,列表每个子项都是一个 Widget

基于上面的基本思路,我们只需要将 ListView.children 构造出来即可:

上面已经有了 mock 数据,只要读取 mock 数据并且构造列表即可:

// 引入 mock 数据
import 'mock/list.dart' as newsList;

1、通过函数生成 List

构造 List 的方法:

  List<Widget> _getNewsList() {
    return newsList.news.map((item) {
      return ListItem(
        title: '${item['title']}',
        subTitle: '${item['time']}',
        cover: '${item['imgurl']}',
      );
    }).toList();
  }

方法 _getNewsList() 最终返回 List<Widget>,其实非常简单就是使用 list.map() 返回一个 Widget 列表,需要注意的是 map 方法返回的是 Iterable,并非 List,需要 toList()

2、设计 ListItem 单个子项

ListItem 是需要我们去构造的自定义 Widget,构造也非常简单,只要传入 封面图、title 和 subtitle 即可

class ListItem extends StatelessWidget {
  ListItem({this.title, this.subTitle, this.cover});

  final String title;
  final String subTitle;
  final String cover;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Container(
          child: Image.network(this.cover, fit: BoxFit.cover),
          width: 60,
          height: 60,
          color: Colors.grey),
      trailing: Icon(Icons.chevron_right),
      title: Text(this.title),
      subtitle: Text(this.subTitle),
    );
  }
}

3、完整的 List 示例

构造出 ListView.children 之后就可以赋值,因此整个列表的代码如下:

class ListViewDemo extends StatelessWidget {
  // 读取文件 mock 数据
  List<Widget> _getNewsList() {
    return newsList.news.map((item) {
      return ListItem(
        title: '${item['title']}',
        subTitle: '${item['time']}',
        cover: '${item['imgurl']}',
      );
    }).toList();
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      // children: this._getData(20),
      children: this._getNewsList(),
    );
  }
}

最终的效果:

86841-aqrfwttl5ia.png

四、横向列表与列表嵌套

1、横向列表

上面我们使用的都是纵向的列表滚动,而如果将其变成横向,也非常简单,只需要修改 scrollDirection 的值即可:

scrollDirection 默认值是 Axis.vertical ,横向使用 Axis.horizontal

scrollDirection: Axis.horizontal,

3、列表嵌套

ListView 的子项可以是另一个 ListView ,只不过两个嵌套的 ListView 不能是相同的方向

举个例子,ListViewVertival 是一个纵向的 ListView Widget,嵌套在了一个横向的 ListView 中

  return ListView(
    scrollDirection: Axis.horizontal,
    children: <Widget>[
      ListItem(),
      ListItem(),
      ListViewVertival(),
      ListItem(),
      ListItem(),
      ListItem(),
      ListItem(),
    ],
    padding: EdgeInsets.all(12),
  );

4、综合使用

将横向 ListView 和 纵向 ListView 嵌套使用,ListItem 是一个子项,不重复代码,完整代码可以在文章最后获取:

class ListViewDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
  return ListView(
    scrollDirection: Axis.horizontal,
    children: <Widget>[
      ListItem(),
      ListItem(),
      ListViewVertival(),
      ListItem(),
      ListItem(),
      ListItem(),
      ListItem(),
    ],
    padding: EdgeInsets.all(12),
  );
  }
}

class ListViewVertival extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: ListView(
        children: <Widget>[
          ListItem(),
          ListItem(),
          ListItem(),
          ListItem(),
        ],
      ),
      width: 100,
      height: 500,
    );
  }
}

实现的效果:

3.gif

五、代码

基础 ListView 和 ListTile 的各种使用:

使用列表数据动态渲染 ListView:

横向列表与列表嵌套: