Flitter 中的 ListView 和 Android 中的 ListView 有点儿不一样,它可以看作是 Widget 组成的 List,这么形容可能也不准确,你就把它想象成一个可以滚动的 Row 或者 Column 好了,先看源码:

  ListView({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    this.itemExtent,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    List<Widget> children = const <Widget>[],
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  })

看一下各个属性的含义:

  • scrollDirection,滚动方向。
  • reverse,滚动方向是否是阅读方向
  • controller,滚动控制,可以设置滚动位置等
  • primary,是否使用 widget 树中默认的 PrimaryScrollController。当滑动方向为垂直方向(scrollDirection值为 Axis.vertical)并且 controller没有指定时,primary默认为true。(这个属性我也没搞明白是干啥的,后续明白了再来修改吧)。
  • physics,列表滚动至边缘后继续拖动的物理效果,AndroidiOS 效果不同。Android 会呈现出一个波纹状(对应 ClampingScrollPhysics),而 iOS 上有一个回弹的弹性效果(对应BouncingScrollPhysics)。如果你想不同的平台上呈现各自的效果可以使用 AlwaysScrollableScrollPhysics,它会根据不同平台自动选用各自的物理效果。如果你想禁用在边缘的拖动效果,那可以使用 NeverScrollableScrollPhysics
  • shrinkWrap,该属性将决定列表的长度是否仅包裹其内容的长度。当ListView嵌在一个无限长的容器组件中时,shrinkWrap必须为true,否则Flutter会给出警告;
  • padding,列表内边距;
  • itemExtent,子元素长度。当列表中的每一项长度是固定的情况下可以指定该值,有助于提高列表的性能(因为它可以帮助ListView在未实际渲染子元素之前就计算出每一项元素的位置);
  • addAutomaticKeepAlives,该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false
  • addRepaintBoundaries,该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false
  • addSemanticIndexes,这个是跟语音识别辅助工具有关系的。
  • cacheExtent,预渲染区域长度,ListView 会在其可视区域的两边留一个 cacheExtent 长度的区域作为预渲染区域(对于ListView.buildListView.separated构造函数创建的列表,不在可视区域和预渲染区域内的子元素不会被创建或会被销毁);
  • children,容纳子元素的组件数组。
  • semanticChildCount,真实的语义数量。
  • dragStartBehavior,确定处理拖动开始行为的方式。

属性解释

因为是 Flutter 初学者,我就先从最简单的例子来做起吧,假如后期学到了更深的内容,再来更新。

  • children,没啥好说的,ListView Item 的集合

先来一个最基础的 ListView,既然 ListView 的 children 接收的是一个 Widget 类型的 List,那我们先来创建一个方法,可以返回一个 List,每个 Widget 就是一个 Item:

  List<Widget> getList() {
    var list = new List<Widget>();
    for (var i = 0; i < 100; i++) {
      list.add(
        Container(
          alignment: Alignment.center,
          width: double.infinity,
          height: 50,
          margin: EdgeInsets.fromLTRB(10, 1, 10, 1),
          color: Colors.lightGreenAccent,
          child: Text("哈哈哈${i}"),
        ),
      );
    }
    return list;
  }

然后直接给 children 属性设置即可;

        body: ListView(
          children: getList(),
        ));

显示效果如下:

就是这么简单(确实比 Android 中设置各种 adapter 之类的简单很多哈)

  • scrollDirection 设置滚动方向。

默认是纵向滚动的,想要横向滚动的话,就可以将 scrollDirection 设置为 Axis.horizontal,但是需要注意的是,如果想要设置横向滚动,需要为 Item 的 Widget 指定一个明确的值,否则会报错:

  List<Widget> getList() {
    var list = new List<Widget>();
    for (var i = 0; i < 100; i++) {
      list.add(
        Container(
          alignment: Alignment.center,
            //为宽设定明确的值
          width: 100,
          height: 10,
          margin: EdgeInsets.fromLTRB(10, 1, 10, 1),
          color: Colors.lightGreenAccent,
          child: Text("哈哈哈${i}"),
        ),
      );
    }
    return list;
  }

还需要注意的是,将 ListView 改为横向的时候,它的高默认是占满布局的(及时你设置了数值也没有用,例如上面设置了 Widget 的 height 为 10,但还是会占满屏幕,但是你可以通过将 ListView 放在一个控制了高度的容器内,来达到控制 ListView 高度的目的):

        body: Container(
          height: 500,
          child: ListView(
            scrollDirection: Axis.horizontal,
            children: getList(),
          ),
        ));

效果如下:

  • reverse ,将该属性设置为 true,效果就变成了这样:

你会发现内容反过来了。

  • controller

再设置一下 controller 属性,这个属性其实作用很大,其参数是一个 ScrollController,可以用来监听 ListView 的滚动等信息,但这里先使用一下最简单的:

        body: Container(
          child: ListView(
            controller:ScrollController(initialScrollOffset: 300) ,
            children: getList(),
          ),
        ));

可以发现 ListView 是预先滚动到了 300 的位置。

  • itemExtent 设置 Item 的高度(如果是横向滚动的话,就是 Item 的宽度)

    这个没啥好说的,就不上图了,但是需要注意一点,不管你 List 中的 Widget 设置的宽高是多少,但如果你设置了 itemExtent 属性,那么都会变成这个值。

常用的属性就是这些,其他的等用到了再写吧。

ListView 的构建

上面的例子中都是直接使用 ListView 的构造方法来创建的,Flutter 还提供了其他方法来帮助我们创建一个 ListView:

  • 上面见过的构造方法创建
  • ListView.builder 利用 IndexedWidgetBuilder 来按需构造。
  • 使用 ListView.separated 构造函数,采用两个 IndexedWidgetBuilder:itemBuilder 根据需要构建子项 separatorBuilder 类似地构建出现在子项之间的分隔符子项。
  • 使用 ListView.customSliverChildDelegate 构造,它提供了定制子模型的其他方面的能力。

下面我们分别使用其他三种方法来创建一下

ListView.builder

按照官方文档的说法,该方法适用于有大量 Item 的情况使用,啥也不说了,先看源码:

  ListView.builder({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    this.itemExtent,
    @required IndexedWidgetBuilder itemBuilder,
    int itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  })

可以看到属性跟之前讲的构造函数几乎一样,只是没有 children 属性,反而多了 itemBuilderitemCount 两个属性:

  • itemBuilder 用来创建 ListView Item
  • itemCount 用来指定 Item 创建个数
      body: Container(
        child: ListView.builder(
          itemExtent: 50.0,
          itemCount: 100,
          itemBuilder: (BuildContext context, int index) {
            return new Text("text $index");
          },
        ),
      ),

这里是一个简单的例子,每个 item 都只是一个 Text,一共有 100 个 Item,届时可以通过实际情况自己做更改。

ListView.separated

这个方法主要是用来为 ListView Item 设置分割线用到的,使用起来也很简单,看参数:

  ListView.separated({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required IndexedWidgetBuilder itemBuilder,
    @required IndexedWidgetBuilder separatorBuilder,
    @required int itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
  })

其他参数没啥好说的,主要是这个 separatorBuilder,它就是来设置分割线的,看代码:

body: Container(
    child: ListView.separated(
        itemBuilder: (BuildContext context, int index) {
            return Container(
                alignment: Alignment.center,
                height: 50,
                child: Text("text $index"),
            );
        },
        separatorBuilder: (BuildContext context, int index) {
            return Divider(
                height: 3.5,
                indent: 20,
                endIndent: 40,
                color: Colors.red,
            );
        },
        itemCount: 100))

效果如下:

ListView.custom

看名称就知道这个方法是用来自定义布局的,暂时没觉得有多大用,就拿官方 API 中的例子凑个数吧:

class MyListView extends StatefulWidget {
  @override
  _MyListViewState createState() => _MyListViewState();
}

class _MyListViewState extends State<MyListView> {
  List<String> items = <String>['1', '2', '3', '4', '5'];

  void _reverse() {
    setState(() {
      items = items.reversed.toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ListView.custom(
          childrenDelegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
              return KeepAlive(
                data: items[index],
                key: ValueKey<String>(items[index]),
              );
            },
            childCount: items.length,
            findChildIndexCallback: (Key key) {
              final ValueKey valueKey = key;
              final String data = valueKey.value;
              return items.indexOf(data);
            }
          ),
        ),
      ),
      bottomNavigationBar: BottomAppBar(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            FlatButton(
              onPressed: () => _reverse(),
              child: Text('Reverse items'),
            ),
          ],
        ),
      ),
    );
  }
}

class KeepAlive extends StatefulWidget {
  const KeepAlive({Key key, this.data}) : super(key: key);

  final String data;

  @override
  _KeepAliveState createState() => _KeepAliveState();
}

class _KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin{
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Text(widget.data);
  }
}

ListTile

在之前的例子中,Item 布局都是我们自己写的,Flutter 中也提供了一个工具,以便我们创建一些简单布局的 Item —— ListTile。

  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,
  })
  • leading,Item 最左侧的内容,Widget类型
  • title,标题
  • subtitle,副标题
  • trailing,最右侧的部分,Widget类型
  • isThreeLine,是否默认3行高度,subtitle不为空时才能使用,如果 subtitle 为空,该属性设置为 true 会报错。
  • dense,会让文本更小、更紧凑
  • contentPadding,Item 的内边距
  • enabled,标识该 Item 是否是可交互的(如果为 false,则 onTap 和 onLongPress 无效,并且颜色变成当前主体设定的禁用颜色)
  • onTap,点击事件
  • onLongPress,长按事件
  • selected,如果选中列表的 item 项,那么文本和图标的颜色将成为主题的主颜色。

一个简单的例子:

        body: Container(child:
            ListView.builder(itemBuilder: (BuildContext context, int index) {
          return ListTile(
            leading: Container(
              width: 50,
              height: 50,
              child: Icon(Icons.account_circle),
              alignment: Alignment.center,
            ),
            title: Text("哈哈哈 ${index}"),
            subtitle: Text("嘻嘻嘻 ${index}"),
            trailing: Icon(Icons.chevron_right),
            onTap: (){
              Scaffold.of(context).showSnackBar(new SnackBar(
                content: new Text("点击了第 " + (index + 1).toString() + " 个 Item"),
                backgroundColor: Colors.amber,
                elevation: 20,
                shape: new RoundedRectangleBorder(
                    borderRadius: new BorderRadius.circular(30.0)),
                behavior: SnackBarBehavior.floating,
                action: new SnackBarAction(
                  label: "知道了",
                  onPressed: () {
                    //移除 Snackbar
                    Scaffold.of(context).removeCurrentSnackBar();
                  },
                ),
                duration: Duration(seconds: 5),
              ));
            },
          );
        }))

其效果是这样的:

ListView 就先到这里了,当然 ListView 最常用的功能并不是只有这些,还有譬如上拉加载下拉刷新侧滑菜单之类的功能, 后面等我学会了再继续写吧。

ListView 的点击事件

上面的例子中,只有 ListTile 有一个 onTap 和 onLongPress 参数来处理点击事件,在 ListView 的各种创建方法中并没有类似于 Android 中的那种 OnItemClick 之类的方法,事实上,在 Flutter 中的点击事件以及其他手势处理都是由 GestureRecognizer(手势识别)GestureDetector(手势检测) 来实现的,可以看这里 延伸知识点:手势处理

results matching ""

    No results matching ""