一,概述
列表是前端最常見的需求。 在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 ; }, ); }
五,官方文檔
官方文檔