Flutter - Listview

什么是基于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 children = const [],
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
})

属性

scrollDirection

scrollDirection:决定子组件的滚动方向,默认是垂直方向
scrollDirection:Axis.horizontal,水平方向
scrollDirection:Axis.vertical,垂直方向

reverse

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

primary

primary:当内容不足以滚动时,是否支持滚动;值为true或者false

controller

此属性接收一个ScrollController对象。ScrollController的主要作用是控制滚动位置和监听滚动事件。

physics

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

shrinkWrap

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

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,则表示无限列表

itemExtent

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

demo

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 {
  @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.separated效果图

无限加载列表

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


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

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

class _CategoryPageState extends State {
  static const loadingTag = "##loading##"; //表尾标记
  var _words = [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(() {
        //重新构建列表
      });
    });
  }
}
上拉加载,下拉刷新

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参数可以接受任何小组件,但通常是文本小组件
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

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,来禁止点击事件

** ListTile的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 {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('ListTile'),),
        body: Column(
      children: [
        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
          },
        )
      ],
    ));
  }
}
ListTile运行效果

添加固定列表头


很多时候需要给列表添加一个固定表头
需要让ListView自动拉伸以适应屏幕,这个时候就需要我们使用到弹性布局Flex
可以使用Expanded自动拉伸组件大小,Column是继承自Flex的,所以可以直接使用Column+Expanded来实现

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

固定列表头

4种构建方式

  1. listview - 这种构建方式是把固定数据变成 item 传给 childen
  Widget build(BuildContext context) {
    // TODO: implement build
    return ListView(
      padding: EdgeInsets.all(20),
       children: [
         Text("AA"),
         Text("BB"),
         Text("CC"),
       ],
    );
  }
  1. ListView.builder - 构建一种 item 类型的列表,当然在 itemBuilder 使用 if、slse 也是能支持多类型 item 的,itemCount 是列表数量
  Widget build(BuildContext context) {
    // TODO: implement build
    return ListView.builder(
      padding: EdgeInsets.all(20),
      itemCount: 50,
      itemBuilder: (context, index) {
        return Text("item:${index}");
      },
    );
  }
  1. ListView.separated - 带分割线的列表, Flutter中把分割线也是看成一个 widget 来处理的,这里新增了 separatorBuilder 来返回分割线 widget,不过分割线的特点是:不包含最后一项,也就是分割线不会出列表的范围。这中设计我很喜欢,把分割线或是分隔符看成列表的 item,可以极大的方便我们设计列表,可玩性灵活性可以大大增加,至少我们可以根据 index 找到前后 item 的类型,然后考虑可以采取不同类型的分隔 item,最典型的应用就是插入广告了,不用对列表有任何修改
  Widget build(BuildContext context) {
    // TODO: implement build
    return ListView.separated(
      padding: EdgeInsets.all(20),
      itemCount: 50,
      itemBuilder: (context, index) {
        return Text("item:${index}");
      },
      separatorBuilder: (context,index){
        return Container(
          height: 1,
          color: Colors.pink,
        );
      },
    );
  }
  1. ListView.custom() - 需要传入一个实现了 SliverChildDelegate 的组件,如 SliverChildListDelegate 和 SliverChildBuilderDelegate。这里我不详细说,因为没必要,SliverChildListDelegate 就是 listview,SliverChildBuilderDelegate 就是 ListView.builder。这个 ListView.custom 用的也比较少



参考资料
Flutter 滚动控件篇-->ListView
Flutter - Listview 详解
flutter - listView 下拉刷新 上拉加载
Flutter 滚动控件篇-->滚动监听及控制(ScrollController)

你可能感兴趣的:(Flutter - Listview)