flutter-widget-ListView

379 阅读7分钟

这是一个滚动列表,它在主轴方向可以滚动,在交叉轴方向则是填满listview,支持基于Sliver的延迟构建模型。它是继承自BoxScrollView,它的构造方式主要有以下几种:默认List,ListView.builder,ListView.seperated,ListView.custom。

默认List:把数据Iterable添加到列表中,之后直接添加ListView中即可,但他只适合内容较少的情况,因为它是一次性渲染所有的items,当item数目较多时很容易出现卡顿。 listview的构造函数如下

ListView({
  Key key,

  //可滚动组件的通用属性
  //滚动方向
  Axis scrollDirection = Axis.vertical,
  //滚动方向是否与正常的相反(比如纵向滑动,reverse=true时是向下滑动,正常reverse=false应该向上滑动)
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  //内边距
  EdgeInsetsGeometry padding,

//ListView各个构造函数的共同参数
  bool shrinkWrap = false,
  this.itemExtent,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  bool addSemanticIndexes = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
  int semanticChildCount,
}) : childrenDelegate = SliverChildListDelegate(
       children,
       addAutomaticKeepAlives: addAutomaticKeepAlives,
       addRepaintBoundaries: addRepaintBoundaries,
       addSemanticIndexes: addSemanticIndexes,
     ), super(
  key: key,
  scrollDirection: scrollDirection,
  reverse: reverse,
  controller: controller,
  primary: primary,
  physics: physics,
  shrinkWrap: shrinkWrap,
  padding: padding,
  cacheExtent: cacheExtent,
  semanticChildCount: semanticChildCount ?? children.length,
);

属性如下:

  • controller:此属性接受一个ScrollController对象。ScrollController的主要作用是控制滚动位置和监听滚动事件。默认情况下,Widget树中会有一个默认的PrimaryScrollController,如果子树中的可滚动组件没有显式的指定controller,并且primary属性值为true时(默认就为true),可滚动组件会使用这个默认的PrimaryScrollController。这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能。
  • primary:这是否是与父级[PrimaryScrollController]关联的主滚动视图。
  • physics:此属性接受一个ScrollPhysics类型的对象,它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。默认情况下,Flutter会根据具体平台分别使用不同的ScrollPhysics对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在iOS上会出现弹性效果,而在Android上会出现微光效果。如果你想在所有平台下使用同一种效果,可以显式指定一个固定的ScrollPhysics,Flutter SDK中包含了两个ScrollPhysics的子类,他们可以直接使用:
    • ClampingScrollPhysics:Android下微光效果。
    • BouncingScrollPhysics:iOS下弹性效果。
  • shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
  • itemExtent:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。
  • addAutomaticKeepAlives:该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。
  • addRepaintBoundaries:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。
  • addSemanticIndexes: 是否在[IndexedSemantics]中包装每个孩子。通常,必须为滚动容器中的子级添加语义索引,以生成正确的可访问性声明。 仅当[IndexedChildSemantics]小部件已提供索引时,才应将其设置为false。

上图的实现的实现代码为

Widget ListviewCreatedByDefault(){
  return ListView(
    shrinkWrap: true,
    padding: EdgeInsets.all(20.0),
    children: <Widget>[
      Text('I\'m dedicating every day to you'),
      ListTile(
        leading:  Icon(Icons.map),
        title:  Text('Maps'),
      ),
      Text('Domestic life was never quite my style'),
      ListTile(
        leading:  Icon(Icons.photo_album),
        title:  Text('Album'),
      ),
      Text('When you smile, you knock me out, I fall apart'),
      ListTile(
        leading:  Icon(Icons.phone),
        title:  Text('Phone'),
      ),
      Text('And I thought I was so smart'),
    ],
  );
}

这种使用方式与SingleChildScrollView+Column的方式没有本质区别,它没有应用基于sliver的懒加载模型。

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

ListView.builder({
...
  @required IndexedWidgetBuilder itemBuilder,
  int itemCount,
}) 

与默认方式的构造器只差两个参数,这里的itemBuilder即为生成对应位置item的函数(Widget Function(BuildContext context, int index);)可以看到这个函数有两个入参,而出参是一个widget。itemCount则为列表项数量,如果为null则为无限列表。可滚动组件的构造函数如果需要一个列表项Builder,那么通过该构造函数构建的可滚动组件通常就是支持基于Sliver的懒加载模型的,反之则不支持。这种方式类似于Android中的recyclerview,它也会自动回收列表项,具体使用如下

Widget ListViewCreatedByBuild() {
  return SizedBox(
    height: 300.0,
    child: ListView.builder(
      scrollDirection: Axis.vertical,
      itemCount: 10, // item 的个数
      itemExtent: 50.0, // 如果为非null,则强制子项在滚动方向上具有给定范围
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("title $index"),
          // item 标题
          leading: Icon(Icons.keyboard),
          // item 前置图标
          subtitle: Text("subtitle $index"),
          // item 副标题
          trailing: Icon(Icons.keyboard_arrow_right),
          // item 后置图标
          isThreeLine: false,
          // item 是否三行显示
          dense: false,
          // item 直观感受是整体大小
          contentPadding: EdgeInsets.all(10.0),
          // item 内容内边距
          enabled: true,
          onTap: () {
            print('点击:$index');
          },
          // item onTap 点击事件
          onLongPress: () {
            print('长按:$index');
          },
          // item onLongPress 长按事件
          selected: false, // item 是否选中状态
        );
      },
    ),
  );
}

效果为

这里我定义了它的高度为300,并且通过index来设置显示。

ListView.seperated:相比build,仅仅多了一个seperaterBuilder可以在生成的列表项之间添加一个分割组件。比如下面的代码

Widget ListViewCreatedBySeperated() {
  return SizedBox(
    height: 300.0,
    child:  ListView.separated(
      scrollDirection: Axis.vertical,
      itemCount: 10, // item 的个数
      separatorBuilder: (BuildContext context, int index) => Divider(height:1.0,indent:10,color: Colors.blue),  // 添加分割线
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title:  Text("title $index"), // item 标题
          leading: Icon(Icons.keyboard), // item 前置图标
          subtitle: Text("subtitle $index"), // item 副标题
          trailing: Icon(Icons.keyboard_arrow_right),// item 后置图标
          isThreeLine:false,  // item 是否三行显示
          dense:true,                // item 直观感受是整体大小
          contentPadding: EdgeInsets.all(10.0),// item 内容内边距
          enabled:true,
          onTap:(){print('点击:$index');},// item onTap 点击事件
          onLongPress:(){print('长按:$index');},// item onLongPress 长按事件
          selected:false,     // item 是否选中状态
        );
      },
    ),
  );
}

实现效果为

比如我们要实现一个上拉加载的无限列表,而且列表上面带一个标题,页面剩余空间让listview全部占用,这时候我们就可以用column+expanded实现,而且为了实现类似于Android中关注列表的效果(点击关注、再点击取消),这里通过调用setState来重新绘制这个view

class _InfiniteListViewState extends State<InfiniteListView> {
  static const loadingTag = "##loading##";
  var _words = <String>[loadingTag];
  var _testDatas = <TestData>[];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ListView.seperated"),
      ),
      body: Column(
        children: <Widget>[
          ListTile(title: Text("测试sectionTitle")),
          Expanded(
            child: ListView.separated(
                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(
                      onTap: () {
                        _testDatas[index].isClicked=!_testDatas[index].isClicked;
                        setState(() {

                        });
                      },
                      title: Text(_testDatas[index].isClicked
                          ? "刚才点击我了"
                          : _testDatas[index].name));
                },
                separatorBuilder: (context, index) => Divider(height: .0),
                itemCount: _words.length),
          )
        ],
      ),
    );
  }

  void _retrieveData() {
    Future.delayed(Duration(seconds: 2)).then((e) {
      _words.insertAll(
          _words.length - 1,
          //每次生成20个单词
          generateWordPairs().take(20).map((e) => e.asPascalCase).toList());
      for (var i = 0; i < 20; i++) {
        _testDatas.add(new TestData("test data number:" + i.toString(), i));
      }
      setState(() {
        //重新构建列表
      });
    });
  }
}

效果为