一,概述
列表是前端最常见的需求。 在flutter中,用ListView来显示列表页,支持垂直和水平方向展示,通过一个属性我们就可以控制其方向,列别有以下分类
- 水平列表
- 垂直列表
- 数据量非常大的列表
- 矩阵式的列表
二,构造函数
构造方法有四种
- new ListView
- 解释
默认构造函数采用子类的显式。此构造函数适用于具有少量(有限个)子项的列表视图,因为构造List需要为可能在列表视图中显示的每个子项执行工作,而不仅仅是那些实际可见的子项。 - 构造函数
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, }) : 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, );
-
适用场景
已知有限个Item的情况下
- 解释
- new ListView.builder
- 解释
它构造函数采用IndexedWidgetBuilder它根据需要构建子项。此构造函数适用于具有大量(或无限)子项数的列表视图,因为仅为实际可见的子项调用构建器。 - 构造函数
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, }) : childrenDelegate = SliverChildBuilderDelegate( itemBuilder, childCount: itemCount, 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 ?? itemCount, );
- 适用场景
长列表时采用builder模式,能提高性能。不是把所有子控件都构造出来,而是在控件viewport加上头尾的cacheExtent这个范围内的子Item才会被构造。在构造时传递一个builder,按需加载是一个惯用模式,能提高加载性能。 - 示例代码
List<Widget> _list = new List(); for (int i = 0; i < strItems.length; i++) { _list.add(buildListData(context, strItems[i], iconItems[i])); } // 添加分割线 var divideList = ListTile.divideTiles(context: context, tiles: _list).toList(); body: new Scrollbar( child: new ListView( // 添加ListView控件 // children: _list, // 无分割线 children: divideList, // 添加分割线 ), );
- 解释
- new ListView.separated
- 解释
它的构造函数有两个IndexedWidgetBuilder 构建器:itemBuilder
根据需要构建子项,separatorBuilder
类似地构建出现在子项之间的分隔子项。此构造函数适用于具有固定数量子项的列表视图。 - 构造函数
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, }) : assert(itemBuilder != null), assert(separatorBuilder != null), assert(itemCount != null && itemCount >= 0), itemExtent = null, childrenDelegate = SliverChildBuilderDelegate( (BuildContext context, int index) { final int itemIndex = index ~/ 2; 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: _computeSemanticChildCount(itemCount), addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, addSemanticIndexes: addSemanticIndexes, semanticIndexCallback: (Widget _, int index) { return index.isEven ? index ~/ 2 : null; } ), super( key: key, scrollDirection: scrollDirection, reverse: reverse, controller: controller, primary: primary, physics: physics, shrinkWrap: shrinkWrap, padding: padding, cacheExtent: cacheExtent, semanticChildCount: _computeSemanticChildCount(itemCount), );
- 适用场景: 列表中需要分割线时,可以自定义复杂的分割线
- 示例代码:
child: new ListView.separated( itemCount: iconItems.length, separatorBuilder: (BuildContext context, int index) => new Divider(), // 添加分割线 itemBuilder: (context, item) { return buildListData(context, strItems[item], iconItems[item]); }, ),
- 解释
- new ListView.custom
- 解释
构造需要SliverChildDelegate提供自定义子项的其他方面的能力。例如,SliverChildDelegate可以控制用于估计实际上不可见的子项大小的算法。 - 构造函数
const ListView.custom({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, this.itemExtent, @required this.childrenDelegate, double cacheExtent, int semanticChildCount, }) : assert(childrenDelegate != null), super( key: key, scrollDirection: scrollDirection, reverse: reverse, controller: controller, primary: primary, physics: physics, shrinkWrap: shrinkWrap, padding: padding, cacheExtent: cacheExtent, semanticChildCount: semanticChildCount, );
- 适用场景:上面几种模式基本可以满足业务需求,如果你还想做一些其它设置(如列表的最大滚动范围)或获取滑动时每次布局的子Item范围,可以尝试custom模式
- 示例代码:
List<Widget> _list = new List(); @override Widget build(BuildContext context) { for (int i = 0; i < strItems.length; i++) { _list.add(buildListData(context, strItems[i], iconItems[i])); } var divideList = ListTile.divideTiles(context: context, tiles: _list).toList(); return new Scaffold( body: new Scrollbar( // 默认方式 List // child: new ListView( // children: divideList, //添加ListView控件 // ), // ListView.separated 方式 // child: new ListView.separated( // itemCount: iconItems.length, // separatorBuilder: (BuildContext context, int index) => new Divider(), // itemBuilder: (context, item) { // return buildListData(context, strItems[item], iconItems[item]); // }, // ), // ListView.builder 方式 child: new ListView.builder( itemCount: iconItems.length, itemBuilder: (context, item) { return new Container( child: new Column( children: <Widget>[ buildListData(context, strItems[item], iconItems[item]), new Divider() ], ), ); }, ), // child: new ListView.custom( // // ), ), ); }
- 解释
- ListTitle
- Flutter 提供了一种常见的列表 item 样式,可以包括前后图标以及大小标题的样构造函数
-
const ListTile({ Key key, this.leading, // item 前置图标 this.title, // item 标题 this.subtitle, // item 副标题 this.trailing, // item 后置图标 this.isThreeLine = false, // item 是否三行显示 this.dense, // item 直观感受是整体大小 this.contentPadding, // item 内容内边距 this.enabled = true, this.onTap, // item onTap 点击事件 this.onLongPress, // item onLongPress 长按事件 this.selected = false, // item 是否选中状态 })
三,参数解析
- Key key
- 解释:key值
- Axis scrollDirection:Axis.vertical
- 解释:ListView 的方向,为 Axis.vertical 表示纵向,为 Axis.horizontal 表示横向。
- bool reverse:false
- 解释:是否反向滚动
- ScrollController controller
- 解释:滑动监听,值为一个 ScrollController 对象,这个属性应该可以用来做下拉刷新和上垃加载
- bool primary
- 解释:是否是与父级PrimaryScrollController关联的主滚动视图。如果primary为true,controller必须设置
- ScrollPhysics physics
- 解释:
设置 ListView 如何响应用户的滑动行为,值为一个 ScrollPhysics 对象,它的实现类常用的有:AlwaysScrollableScrollPhysics:总是可以滑动。
NeverScrollableScrollPhysics:禁止滚动。
BouncingScrollPhysics:内容超过一屏,上拉有回弹效果。
ClampingScrollPhysics:包裹内容,不会有回弹,感觉跟 AlwaysScrollableScrollPhysics 差不多。
- 解释:
- bool shrinkWrap:false
- EdgeInsetsGeometry padding
- 解释:滚动视图与子项之间的内边距
- double itemExtent
- 解释:子项范围
- @ required indexedWidgetBuilder
- 解释:itemBuilder 位置构建器
- 解释:itemBuilder 位置构建器
- int itemCount
- 解释:子 Item 数量,只有使用 new ListView.builder() 和 new ListView.separated() 构造方法的时候才能指定,其中 new ListView.separated() 是必须指定。
- 解释:子 Item 数量,只有使用 new ListView.builder() 和 new ListView.separated() 构造方法的时候才能指定,其中 new ListView.separated() 是必须指定。
- bool addAutomaticKeepAlives:true
- 解释:对应于 SliverChildBuilderDelegate.addAutomaticKeepAlives属性。即是否将每个子项包装在AutomaticKeepAlive中。
- 解释:对应于 SliverChildBuilderDelegate.addAutomaticKeepAlives属性。即是否将每个子项包装在AutomaticKeepAlive中。
- bool addRepaintBoundaries:true
- 解释:对应于 SliverChildBuilderDelegate.addRepaintBoundaries属性。是否将每个子项包装在RepaintBoundary中
- 解释:对应于 SliverChildBuilderDelegate.addRepaintBoundaries属性。是否将每个子项包装在RepaintBoundary中
- bool addSemanticIndexes:true
- 解释:对应于 SliverChildBuilderDelegate.addSemanticIndexes属性。是否将每个子项包装在IndexedSemantics中。
- 解释:对应于 SliverChildBuilderDelegate.addSemanticIndexes属性。是否将每个子项包装在IndexedSemantics中。
- double cacheExtent
- child
- 解释:高度会适配 item填充的内容的高度,我们非常的不希望child的高度固定,因为这样的话,如果里面的内容超出就会造成布局的溢出。
- int semanticChildCount
- 解释:提供语义信息的孩子的数量
四,示例demo
import 'package:flutter/material.dart'; //void main() => runApp(ListViewDemo()); class BaseBean { String name; int age; String content; BaseBean(this.name, this.age, this.content); } List<BaseBean> list = new List<BaseBean>.generate( 60, (i) => new BaseBean("name$i", i, "content=$i")); class ListViewDemo extends StatefulWidget { @override _ListViewDemoState createState() => new _ListViewDemoState(); } class _ListViewDemoState extends State<ListViewDemo> { List<BaseBean> list; @override void initState() { // TODO: implement initState super.initState(); list = new List<BaseBean>.generate( 60, (i) => new BaseBean("name$i", i, "content=$i")); } @override Widget build(BuildContext context) { return new MaterialApp( home: new Scaffold( appBar: new AppBar( centerTitle: true, title: new Text("ListView"), ), body: // listViewDefault(list)) listViewListTile(list)) // listViewLayoutBuilder(list)), // listViewLayoutCustom(list)), // listViewLayoutSeparated(list)), ); } ///默认构建 Widget listViewDefault(List<BaseBean> list) { List<Widget> _list = new List(); for (int i = 0; i < list.length; i++) { _list.add(new Center( child: new Text(list[i].age.toString()), )); } // 添加分割线 var divideList = ListTile.divideTiles(context: context, tiles: _list).toList(); return new Scrollbar( child: new ListView( // 添加ListView控件 children: _list, // 无分割线 // children: divideList, // 添加分割线/ ), ); } /// ListTile Widget listViewListTile(List<BaseBean> list) { List<Widget> _list = new List(); for (int i = 0; i < list.length; i++) { _list.add(new Center( child: ListTile( leading: new Icon(Icons.list), title: new Text(list[i].name), trailing: new Icon(Icons.keyboard_arrow_right), ), )); } return new ListView( children: _list, ); } ///listView builder 构建 Widget listViewLayoutBuilder(List<BaseBean> list) { return ListView.builder( //设置滑动方向 Axis.horizontal 水平 默认 Axis.vertical 垂直 scrollDirection: Axis.vertical, //内间距 padding: EdgeInsets.all(10.0), //是否倒序显示 默认正序 false 倒序true reverse: false, //false,如果内容不足,则用户无法滚动 而如果[primary]为true,它们总是可以尝试滚动。 primary: true, //确定每一个item的高度 会让item加载更加高效 itemExtent: 50.0, //item 高度会适配 item填充的内容的高度 多用于嵌套listView中 内容大小不确定 比如 垂直布局中 先后放入文字 listView (需要Expend包裹否则无法显示无穷大高度 但是需要确定listview高度 shrinkWrap使用内容适配不会) 文字 shrinkWrap: true, //item 数量 itemCount: list.length, //滑动类型设置 //new AlwaysScrollableScrollPhysics() 总是可以滑动 NeverScrollableScrollPhysics禁止滚动 BouncingScrollPhysics 内容超过一屏 上拉有回弹效果 ClampingScrollPhysics 包裹内容 不会有回弹 // cacheExtent: 30.0, //cacheExtent 设置预加载的区域 cacheExtent 强制设置为了 0.0,从而关闭了“预加载” physics: new ClampingScrollPhysics(), //滑动监听 // controller , itemBuilder: (context, i) => new Container( child: new Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ new Text( "${list[i].name}", style: new TextStyle(fontSize: 18.0, color: Colors.red), ), new Text( "${list[i].age}", style: new TextStyle(fontSize: 18.0, color: Colors.green), ), new Text( "${list[i].content}", style: new TextStyle(fontSize: 18.0, color: Colors.blue), ), ], ), )); } // ignore: slash_for_doc_comments /** * ListView({ List<Widget> children, }) ListView.builder({ int: itemCount, IndexedWidgetBuilder itemBuilder, }) ListView.custom({ SliverChildDelegate childrenDelegate, }) 大家可能对前两种比较熟悉,分别是传入一个子元素列表或是传入一个根据索引创建子元素的函数。 其实前两种方式都是第三种方式的“快捷方式”。因为 ListView 内部是靠这个 childrenDelegate 属性动态初始化子元素的。 */ ///listView custom 构建 Widget listViewLayoutCustom(list) { // return ListView.custom(childrenDelegate: new MyChildrenDelegate()); return ListView.custom( itemExtent: 40.0, childrenDelegate: MyChildrenDelegate( (BuildContext context, int i) { return new Container( child: new Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ new Text( "${list[i].name}", style: new TextStyle(fontSize: 18.0, color: Colors.red), ), new Text( "${list[i].age}", style: new TextStyle(fontSize: 18.0, color: Colors.green), ), new Text( "${list[i].content}", style: new TextStyle(fontSize: 18.0, color: Colors.blue), ), ], )); }, childCount: list.length, ), cacheExtent: 0.0, ); } } // ignore: slash_for_doc_comments /** * 继承SliverChildBuilderDelegate 可以对列表的监听 */ class MyChildrenDelegate extends SliverChildBuilderDelegate { MyChildrenDelegate( Widget Function(BuildContext, int) builder, { int childCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, }) : super(builder, childCount: childCount, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries); ///监听 在可见的列表中 显示的第一个位置和最后一个位置 @override void didFinishLayout(int firstIndex, int lastIndex) { print('firstIndex: $firstIndex, lastIndex: $lastIndex'); } ///可不重写 重写不能为null 默认是true 添加进来的实例与之前的实例是否相同 相同返回true 反之false ///listView 暂时没有看到应用场景 源码中使用在 SliverFillViewport 中 @override bool shouldRebuild(SliverChildBuilderDelegate oldDelegate) { // TODO: implement shouldRebuild print("oldDelegate$oldDelegate"); return super.shouldRebuild(oldDelegate); } } /// listView separated 构建 用于多类型 分割 Widget listViewLayoutSeparated(List<BaseBean> list) { return ListView.separated( itemCount: list.length, separatorBuilder: (content, index) { //和itemBuilder 同级别的执行 if (index == 2) { return new Container( height: 40.0, child: new Center( child: new Text("类型1"), ), color: Colors.red, ); } else if (index == 7) { return new Container( height: 40.0, child: new Center( child: new Text("类型2"), ), color: Colors.blue, ); } else if (index == 14) { return new Container( height: 40.0, child: new Center( child: new Text("类型3"), ), color: Colors.yellow, ); } else { return new Container(); } }, itemBuilder: (content, i) { return new InkWell( child: new Container( height: 30.0, child: new Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ new Text( "${list[i].name}", style: new TextStyle(fontSize: 18.0, color: Colors.red), ), new Text( "${list[i].age}", style: new TextStyle(fontSize: 18.0, color: Colors.green), ), new Text( "${list[i].content}", style: new TextStyle(fontSize: 18.0, color: Colors.blue), ), ], )), onTap: () { print("1111"); }, ); // return ; }, ); }
五,官方文档
官方文档