Flutter 中的ListView是最常用的可滚动的组件之一,
可以沿一个方向线性排布所有子组件,并且它也支持基于Sliver的延迟构建模型。

基于Sliver的延迟构建

什么是基于Sliver的延迟构建模型呢?

通常可滚动组件的子组件可能会非常多、占用的总高度也会非常大;如果要一次性将子组件全部构建出将会非常昂贵!为此,Flutter中提出一个Sliver(中文为“薄片”的意思)概念,如果一个可滚动组件支持Sliver模型,那么该滚动可以将子组件分成好多个“薄片”(Sliver),只有当Sliver出现在视口中时才会去构建它,这种模型也称为“基于Sliver的延迟构建模型”。

源码示例

构造函数如下:

ListView({
	...  
	//可滚动组件的公共参数
	Axis scrollDirection = Axis.vertical,
	bool reverse = false,
	ScrollController controller,
	bool primary,
	ScrollPhysics physics,	
	EdgeInsetsGeometry padding,	
	
	//ListView各个构造函数的共同参数
	this.itemExtent,
	bool shrinkWrap = false,
	bool addAutomaticKeepAlives = true,
	bool addRepaintBoundaries = true,
	bool addSemanticIndexes = true,
	double cacheExtent,
	
	//子组件列表
	List<Widget> children = const <Widget>[],
	int semanticChildCount,
	DragStartBehavior dragStartBehavior = DragStartBehavior.start,
})
复制代码

属性解释

scrollDirection

决定子组件的滚动方向(排列方向),默认是垂直方向

scrollDirection:Axis.horizontal,水平方向
scrollDirection:Axis.vertical,垂直方向

reverse

决定滚动方向是否与阅读方向一致

图片加载失败!

primary

当内容不足以滚动时,是否支持滚动;

值为true或者false,我试了一下,好像没什么卵用,不知道是理解错了,还是怎么的,先写上吧

controller

此属性接收一个ScrollController对象。ScrollController的主要作用是控制滚动位置和监听滚动事件
有关ScrollController的使用及详情,请参考Flutter 滚动控件篇-->滚动监听及控制(ScrollController)

默认情况下,Widget树中会有一个默认的PrimaryScrollController,如果子树中的可滚动组件没有显式的指定controller,并且primary属性值为true时(默认就为true),可滚动组件会使用这个默认的PrimaryScrollController。这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为

physics

此属性接受一个ScrollPhysics类型的对象,它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。

在iOS上会出现弹性效果,而在Android上会出现微光效果。

shrinkWrap

该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false
默认情况下,ListView会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true,否则会报错。

itemExtent

该参数如果不为null,则会强制children的“长度”为itemExtent的值;

这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。

addAutomaticKeepAlives

该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive组件中;

在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时也不会被回收,它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false

addRepaintBoundaries

该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。
当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false

addSemanticIndexes

该属性表示是否把子控件包装在IndexedSemantics里,用来提供无障碍语义

cacheExtent

可见区域的前后会有一定高度的空间去缓存子控件,当滑动时就可以迅速呈现

简单的就是说,当你快要滑到加载数据的时候,他已经提前一步加载好了,等到你滑到的时候就会显示出来,而不至于用户滑到的时候还需要等待一会儿。

semanticChildCount

有含义的子控件的数量

如:ListView会用children的长度,而ListView.separated会用children长度的一半

children

这里的children需要说一下,和别的组件里的不一样。

这里的children参数,他就收一个列表,但是这种方式适合只有少量的子组件的情况。因为这种方式需要将所有children都提前创建好(这需要做大量工作),而不是等到子组件真正显示的时候再创建,也就是说通过默认构造函数构建的ListView没有应用基于Sliver的懒加载模型

再次强调,可滚动组件通过一个List来作为其children属性时,只适用于子组件较少的情况,这是一个通用规律。

ListView.builder

上面的children只适合数据较少的情况下使用

ListView.builder则适合列表项比较多(或者无限)的情况下使用,因为只有当子组件真正显示的时候才会被创建,也就说通过该构造函数创建的ListView是支持基于Sliver的懒加载模型的。

源码示例

构造函数如下:

ListView.builder({
  // ListView公共参数已省略  
  ...
  @required IndexedWidgetBuilder itemBuilder,
  int itemCount,
  ...
})
复制代码

属性解释

itemBuilder

它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget(就是一个组件)。当列表滚动到具体的index位置时,会调用该构建器构建列表项,也就是所谓的基于Sliver的懒加载模型。

itemCount

该属性表示列表项的数量,如果为null,则表示无限列表

注:可滚动组件的构造函数如果需要一个列表项Builder,那么通过该构造函数构建的可滚动组件通常就是支持基于Sliver的懒加载模型的,反之则不支持,这是个一般规律。

代码示例:

ListView.builder(
	itemCount: 100,
	itemExtent: 50.0, //强制高度为50.0,如果这个值越来越小的话,那么显示的值是会重叠的
	itemBuilder: (BuildContext context, int index) {
	  return ListTile(title: Text("$index"));
})
复制代码

运行效果:

图片加载失败!

ListView.separated

ListView.separated可以在生成的列表项之间添加一个分割组件。

它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。

代码示例:

在奇数行添加一条蓝色下划线,偶数行添加一条红色下划线。

import 'package:flutter/material.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  @override
  Widget build(BuildContext context) {
    //下划线widget预定义以供复用。
    Widget Lineblue = Divider(color: Colors.blue);
    Widget Linered = Divider(color: Colors.red);
    return Scaffold(
        appBar: AppBar(
          title: Text(
            "ListView.separated",
            // style: TextStyle(color: Color(0xFF1E88E5)),
          ),
        ),
        body: ListView.separated(
          itemCount: 100,
          //列表项构造器
          itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text("$index"));
          },
          //分割器构造器
          separatorBuilder: (BuildContext context, int index) {
            return index % 2 == 0 ? Lineblue : Linered;
          },
        ));
  }
}
复制代码

运行效果:

图片加载失败!

无限加载列表

假设我们要从数据源异步分批拉取一些数据,然后用ListView展示。
当我们滑动到列表末尾时,判断是否需要再去拉取数据,如果是,则去拉取,拉取过程中在表尾显示一个转着的小圆圈,拉取成功后将数据插入列表;如果不需要再去拉取,则在表尾提示"没有更多了"。

这里我们需要安装一个包english_words: ^3.1.5(在pubspec.yaml文件中的dependencies下安装),可以给我们自动的生成英语单词

代码示例:

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  static const loadingTag = "##loading##"; //表尾标记
  var _words = <String>[loadingTag];

  @override
  void initState() {
    super.initState();
    _retrieveData();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: _words.length,
      itemBuilder: (context, index) {
        //如果到了表尾
        if (_words[index] == loadingTag) {
          //不足100条,继续获取数据
          if (_words.length - 1 < 100) {
            //获取数据
            _retrieveData();
            //加载时显示loading
            return Container(
              padding: const EdgeInsets.all(16.0),
              alignment: Alignment.center,
              child: SizedBox(
                  width: 24.0,
                  height: 24.0,
                  child: CircularProgressIndicator(strokeWidth: 2.0)),
            );
          } else {
            //已经加载了100条数据,不再获取数据。
            return Container(
                alignment: Alignment.center,
                padding: EdgeInsets.all(16.0),
                child: Text(
                  "没有更多了",
                  style: TextStyle(color: Colors.grey),
                ));
          }
        }
        //显示单词列表项
        return ListTile(title: Text(_words[index]));
      },
      separatorBuilder: (context, index) => Divider(height: .0),
    );
  }

  void _retrieveData() {
    Future.delayed(Duration(seconds: 2)).then((e) {
      _words.insertAll(
          _words.length - 1,
          //每次生成20个单词
          generateWordPairs().take(20).map((e) => e.asPascalCase).toList());
      setState(() {
        //重新构建列表
      });
    });
  }
}
复制代码

_retrieveData()的功能是模拟从数据源异步获取数据,
english_words包的generateWordPairs()方法可以每次生成20个单词。

运行效果:

图片加载失败!

ListTile

这里我们说一下ListTile
ListTile通常用于在 Flutter 中填充 ListView

源码示例:

构造函数如下:

const ListTile({
	Key key,
	this.leading,
	this.title,
	this.subtitle,
	this.trailing,
	this.isThreeLine = false,
	this.dense,
	this.contentPadding,
	this.enabled = true,
	this.onTap,
	this.onLongPress,
	this.selected = false,
})
复制代码

属性解释

title

title参数可以接受任何小组件,但通常是文本小组件

代码示例:

ListTile(
  title: Text('我喜欢你!'),
)
复制代码

subtitle

他是一个副标题,显示在标题(title)下面较小的文本

代码示例:

ListTile(
  title: Text('我喜欢你!'),
  subtitle: Text('你喜欢我吗?'),
)
复制代码

dense

使文本更小,并将所有内容打包在一起

代码示例:

ListTile(
  title: Text('我喜欢你!'),
  subtitle: Text('你喜欢我吗?'),
  dense:true,
)
复制代码

leading

将图像或图标添加到列表的开头。

代码示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜欢你'),
  subtitle: Text('你喜欢我吗?'),
  dense:true,
)
复制代码

trailing

在列表的末尾放置一个图像。

代码示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜欢你'),
  subtitle: Text('你喜欢我吗?'),
  dense:true,
  trailing: Icon(Icons.keyboard_arrow_right),
)
复制代码

contentPadding

设置内容边距,默认是 16
我这里设置30

代码示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜欢你'),
  subtitle: Text('你喜欢我吗?'),
  dense:true,
  trailing: Icon(Icons.keyboard_arrow_right),
  contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
)
复制代码

selected

如果选中列表的 item 项,那么文本和图标的颜色将成为主题的主颜色。

代码示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜欢你'),
  subtitle: Text('你喜欢我吗?'),
  dense:true,
  trailing: Icon(Icons.keyboard_arrow_right),
  contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
  selected: true,
)
复制代码

onTap、onLongPress

onTap 为单击,onLongPress 为长按。

代码示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜欢你'),
  subtitle: Text('你喜欢我吗?'),
  dense:true,
  trailing: Icon(Icons.keyboard_arrow_right),
  contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
  selected: true,
  onTap: () {
    // do something
  },
  onLongPress: (){
    // do something else
  },
)
复制代码

enabled

通过将 enable 设置为 false,来禁止点击事件

这里就不写代码了,比较简单。

属性demo示例

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('ListTile'),),
        body: Column(
      children: <Widget>[
        ListTile(
          leading: CircleAvatar(
            backgroundImage: AssetImage('images/Test.jpg'),
          ),
          title: Text('我喜欢你'),
          subtitle: Text('你喜欢我吗?'),
          dense: true,
          trailing: Icon(Icons.keyboard_arrow_right),
          contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
          // selected: true,
          onTap: () {
            // do something
          },
          onLongPress: () {
            // do something else
          },
        ),
        ListTile(
          leading: CircleAvatar(
            backgroundImage: AssetImage('images/Test.jpg'),
          ),
          title: Text('我喜欢你'),
          subtitle: Text('你喜欢我吗?'),
          dense: true,
          trailing: Icon(Icons.keyboard_arrow_right),
          contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
          selected: true,
          onTap: () {
            // do something
          },
          onLongPress: () {
            // do something else
          },
        )
      ],
    ));
  }
}
复制代码

运行效果:

图片加载失败!

添加固定列表头

很多时候我们需要给列表添加一个固定表头。

我们需要让ListView自动拉伸以适应屏幕,这个时候就需要我们使用到弹性布局Flex,如果不知道的话,请移步Flutter 布局控件篇-->Flex、Expanded

我们可以使用Expanded自动拉伸组件大小,并且我们也说过Column是继承自Flex的,所以我们可以直接使用Column+Expanded来实现,

代码示例:

Column(children: <Widget>[
  ListTile(title: Text("数字列表")),
  Expanded(
	child: ListView.builder(itemBuilder: (BuildContext context, int index) {
	  return ListTile(title: Text("$index"));
	}),
  ),
]);
复制代码

运行效果:

图片加载失败!


T_T