写点什么

flutter 系列之:flutter 中常用的 ListView layout 详解

作者:程序那些事
  • 2022 年 6 月 13 日
  • 本文字数:3717 字

    阅读完需:约 12 分钟

简介

ListView 是包含多个 child 组件的 widget,在 ListView 中所有的 child widget 都是以 list 的形式来呈现的,你可以自定义 List 的方向,但是和 GridView 不同的是 ListView 中的每一个 List 里面都只包含一个 widget。


今天我们来详细了解一下 ListView 的底层实现和具体的应用。

ListView 详解

和 GridView 一样,ListView 也是继承自 ScrollView,表示它是一个可以滚动的 View。


具体而言,ListView 首先继承自 BoxScrollView:


class ListView extends BoxScrollView
复制代码


然后 BoxScrollView 又继承自 ScrollView:


abstract class BoxScrollView extends ScrollView 
复制代码

ListView 中的特有属性

首先我们来看下 ListView 中的特有属性,ListView 和它的父类相比,多了三个属性,分别是 itemExtent,prototypeItem 和 childrenDelegate。


其中 itemExtent 是一个 double 类型的数据,如果给定的是一个非空值,那么表示的是 child 在 scroll 方向的 extent 大小。这个属性主要用来控制 children 的 extend 信息,这样每个 child 就不需要自行来判断自己的 extend。


使用 itemExtent 的好处在于,ListView 可以统一的在滚动机制上进行优化,从而提升性能表现。


prototypeItem 是一个 widget,从名字就可以看出,这个一个 prototype 的 widget,也就是说是一个原型,其他的 child 可以参照这个原型 widget 的大小进行 extent 的设置。


ListView 中的最后一个自定义属性是 childrenDelegate,这个 childrenDelegate 和 GridView 中的含义是一样的,用来生成 ListView 中 child。


之前我们在讲解 GirdView 的时候有提到过,GirdView 中还有一个自定义的属性叫做 gridDelegate,这个 gridDelegate 是一个 SliverGridDelegate 的实例,用来控制子组件在 GridView 中的布局。


因为 ListView 的子组件的布局是已经确定的,所以就不再需要 gridDelegate 了,这是 ListView 和 GridView 的一大区别。


ListView 作为一个继承的类,需要实现一个 buildChildLayout 的方法:


  @override  Widget buildChildLayout(BuildContext context) {    if (itemExtent != null) {      return SliverFixedExtentList(        delegate: childrenDelegate,        itemExtent: itemExtent!,      );    } else if (prototypeItem != null) {      return SliverPrototypeExtentList(        delegate: childrenDelegate,        prototypeItem: prototypeItem!,      );    }    return SliverList(delegate: childrenDelegate);  }
复制代码


这个方法的实现逻辑和我们之前讲到的三个属性是相关联的,在 buildChildLayout 中,如果 itemExtent 有值的话,因为 itemExtent 本身就是一个固定值,所以返回的是 SliverFixedExtentList。


如果 itemExtent 没有设置,并且 prototypeItem 有值的话,返回的是一个 SliverPrototypeExtentList。


最后,如果 itemExtent 和 prototypeItem 都没有设置的话,返回的是一个 SliverList 对象。

ListView 的构造函数

和 GridView 一样,为了满足我们的多样性的设计需求,ListView 也提供了多个构造函数。


首先我们来看下 ListView 的最基本的构造函数:


ListView({    Key? key,    Axis scrollDirection = Axis.vertical,    bool reverse = false,    ScrollController? controller,    bool? primary,    ScrollPhysics? physics,    bool shrinkWrap = false,    EdgeInsetsGeometry? padding,    this.itemExtent,    this.prototypeItem,    bool addAutomaticKeepAlives = true,    bool addRepaintBoundaries = true,    bool addSemanticIndexes = true,    double? cacheExtent,    List<Widget> children = const <Widget>[],    int? semanticChildCount,    DragStartBehavior dragStartBehavior = DragStartBehavior.start,    ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,    String? restorationId,    Clip clipBehavior = Clip.hardEdge,  })
复制代码


这里 itemExtent 和 prototypeItem 这两个属性是外部传入的,childrenDelegate 是通过其他的参数构造而来的:


 childrenDelegate = SliverChildListDelegate(         children,         addAutomaticKeepAlives: addAutomaticKeepAlives,         addRepaintBoundaries: addRepaintBoundaries,         addSemanticIndexes: addSemanticIndexes,       ),
复制代码


ListView 中所有的 child 组件都在 List Widget 的 children 中。


这个默认的构造函数,适用于 child 比较少的情况,因为需要一次传入所有的 child 组件到 list 中,所以对性能的影响还是挺大的,并且传入的 child 是不可变的。


如果 child 比较多的情况下,就需要使用到其他的构造函数了,比如 ListView.builder。


ListView.builder 使用的是 builder 模式来构建 child 组件,具体而言他的 childrenDelegate 实现如下:


childrenDelegate = SliverChildBuilderDelegate(         itemBuilder,         childCount: itemCount,         addAutomaticKeepAlives: addAutomaticKeepAlives,         addRepaintBoundaries: addRepaintBoundaries,         addSemanticIndexes: addSemanticIndexes,       ),
复制代码


这里的 childrenDelegate 是一个 SliverChildBuilderDelegate,通过传入 itemBuilder 和总的 itemCount 就可以实现动态创建 child 的功能。


在 ListView 的实际使用过程中,为了页面好看或者更有区分度,我们一般会在 list 的 item 中添加一些分隔符 separator,为了自动化实现这个功能,ListView 提供了一个 ListView.separated 的构造函数,用来提供 list item 中间的分隔符。


ListView.separated 需要传入两个 IndexedWidgetBuilder,分别是 itemBuilder 和 separatorBuilder。


下面是 childrenDelegate 的具体实现:


 childrenDelegate = SliverChildBuilderDelegate(         (BuildContext context, int index) {           final int itemIndex = index ~/ 2;           final Widget widget;           if (index.isEven) {             widget = itemBuilder(context, itemIndex);           } else {             widget = separatorBuilder(context, itemIndex);             assert(() {               if (widget == null) {                 throw FlutterError('separatorBuilder cannot return null.');               }               return true;             }());           }           return widget;         },         childCount: _computeActualChildCount(itemCount),         addAutomaticKeepAlives: addAutomaticKeepAlives,         addRepaintBoundaries: addRepaintBoundaries,         addSemanticIndexes: addSemanticIndexes,         semanticIndexCallback: (Widget _, int index) {           return index.isEven ? index ~/ 2 : null;         },       ),
复制代码


可以看到,如果 index 是 even 的话就会使用 itemBuilder 生成一个 widget,如果 index 是 odd 的话,就会使用 separatorBuilder 来生成一个 separator 的 widget。


最后,ListView 还有一个更加开放的构造函数 ListView.custom,custom 和其他构造函数不同的地方在于他可以自定义 childrenDelegate,从而提供了更多的扩展空间。

ListView 的使用

有了上面的构造函数,我们可以很方便的根据自己的需要来使用 ListView,下面是一个简单的使用图片做 child 的例子:


class ListViewApp extends StatelessWidget{  const ListViewApp({Key? key}) : super(key: key);  @override  Widget build(BuildContext context) {    return ListView.builder(      itemCount: 5,      itemBuilder: (BuildContext context, int index) {        return Container(            constraints: const BoxConstraints(maxWidth:100,maxHeight: 100),            child: Image.asset('images/head.jpg')        );      },    );  }}
复制代码


上面的例子中,我们使用的是 ListView.builder 构造函数,返回的 Widget 中,中的 widget 个数是 5,每个 item 是由 itemBuilder 来生成的。


这里我们把 Image 封装在一个 Container 中,并且为 Container 设置了一个 constraints 来控制图片的大小。


最终生成的界面如下:



上面的例子中,item 之间是没有分隔符的,我们可以讲上面的例子进行稍微的修改一下,使用 ListView.separated 来构造 ListView,如下所示:


class ListViewSeparatedApp extends StatelessWidget{
@override Widget build(BuildContext context) { return ListView.separated( itemCount: 10, separatorBuilder: (BuildContext context, int index) => const Divider(), itemBuilder: (BuildContext context, int index) { return Container( constraints: const BoxConstraints(maxWidth:50,maxHeight: 50), child: Image.asset('images/head.jpg') ); }, ); }}
复制代码


这里我们需要传入 separatorBuilder 来作为分隔符,为了简单起见,我们直接使用了 Divider 这个 widget。


最后生成的界面如下:


总结

以上就是 ListView 的介绍和基本的使用。


本文的例子:https://github.com/ddean2009/learn-flutter.git

发布于: 刚刚阅读数: 3
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
flutter系列之:flutter中常用的ListView layout详解_flutter_程序那些事_InfoQ写作社区