
继承关系:
DiagnosticableMixin < Diagnosticable < DiagnosticableTree < Widget < StatelessWidget < ScrollView < BoxScrollView < ListView
滑动列表都继承于 ScrollView 这点和iOS中的UITableView 和 UICollectionView是一样的。
先说下 ScrollView
ScrollView
构造函数
const ScrollView({Key key,this.scrollDirection = Axis.vertical, //滑动方向this.reverse = false,//控制 ScrollView 是从头开始滑,还是从尾开始滑,默认为 false,就是从头开始滑this.controller, //此属性接受一个ScrollController对象。ScrollController的主要作用是控制滚动位置和监听滚动事件。默认情况下,Widget树中会有一个默认的PrimaryScrollController,如果子树中的可滚动组件没有显式的指定controller,并且primary属性值为true时(默认就为true),可滚动组件会使用这个默认的PrimaryScrollController。这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能。bool primary, //是否是与父级关联的主滚动视图.当为 true 时,即使 ScrollView 里没有足够的内容也能滑动,当 ScrollView 为 Axis.vertical,且 controller 为 null时,默认为 trueScrollPhysics physics,// 此属性接受一个ScrollPhysics类型的对象,它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。默认情况下,Flutter会根据具体平台分别使用不同的ScrollPhysics对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在iOS上会出现弹性效果,而在Android上会出现微光效果。如果你想在所有平台下使用同一种效果,可以显式指定一个固定的ScrollPhysics,Flutter SDK中包含了两个ScrollPhysics的子类,他们可以直接使用:ClampingScrollPhysics:Android下微光效果。BouncingScrollPhysics:iOS下弹性效果。this.shrinkWrap = false, //是否根据子组件的总长度来设置ScrollView的长度. 默认值为false 。默认情况下,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。this.center,//[GrowthDirection.forward]滚动方向的第一个子视图.只对[CustomScrollView]有效.this.anchor = 0.0,//滚动偏移量的相对位置.如果[anchor]为0.5,并且[axisDirection]为[AxisDirection.down]或[AxisDirection.up],则零滚动偏移量将在视口内垂直居中。如果[anchor]为1.0,并且[axisDirection]为[AxisDirection.right],则零滚动偏移位于视口的左边缘this.cacheExtent, //缓存范围this.semanticChildCount, //将提供语义信息的子代数量this.dragStartBehavior = DragStartBehavior.start, //确定处理拖动开始行为的方式。如果设置为[DragStartBehavior.start],则在检测到拖动手势时将开始滚动拖动行为如果设置为[DragStartBehavior.down],它将在首次检测到向下事件时开始})
BoxScrollView
构造函数
const BoxScrollView({Key key,Axis scrollDirection = Axis.vertical,bool reverse = false,ScrollController controller,bool primary,ScrollPhysics physics,bool shrinkWrap = false,this.padding, //内间距double cacheExtent,int semanticChildCount,DragStartBehavior dragStartBehavior = DragStartBehavior.start,})
对比ScrollView多了一个内间距
ListView
构造函数
ListView({Key key,Axis scrollDirection = Axis.vertical,bool reverse = false,ScrollController controller,bool primary,ScrollPhysics physics,bool shrinkWrap = false,EdgeInsetsGeometry padding,**this.itemExtent,//该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。bool addAutomaticKeepAlives = true,//该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。bool addRepaintBoundaries = true, //该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。bool addSemanticIndexes = true, //是否用 IndexedSemantics 来包列表项,默认为 true使用 IndexedSemantics 是为了正确的用于辅助模式double cacheExtent,List<Widget> children = const <Widget>[],int semanticChildCount,DragStartBehavior dragStartBehavior = DragStartBehavior.start,})
列子:
下图应该是简单的加载了.

import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';class SchoolPage extends StatefulWidget {@override_SchoolPageState createState() => _SchoolPageState();}class _SchoolPageState extends State<SchoolPage> {//获取数据List<Widget> _getDataSoure() {List<Widget> _list = List();for(var i = 0 ; i < 50 ; i++ ) {_list.add(ListTile(//添加内容title: Text('hello flutter $i',style: TextStyle(fontSize: 15,color: Colors.red),),leading: Image.network("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=371568753,606618613&fm=26&gp=0.jpg"),));}return _list;}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("List learn"),),body: ListView(//加载数据源children: _getDataSoure(),));}}
里面的每个item都是利用 ListTitle 加载的…来看看ListTitle
ListTitle
class ListTile extends StatelessWidget {const ListTile({Key key,this.leading, //显示在标题左边的widgetthis.title, //titlethis.subtitle, //subtitlethis.trailing, //显示在标题右边的widgetthis.isThreeLine = false, //是否显示三行文本. 为true时,subtitle不能为空this.dense,this.contentPadding, //内容的边距this.enabled = true, //是否可用this.onTap, //点击事件this.onLongPress, //长按事件this.selected = false,//是否选中})
例子:
把上面的列表加个 subtitle和trailong
//获取数据List<Widget> _getDataSoure() {List<Widget> _list = List();for(var i = 0 ; i < 50 ; i++ ) {_list.add(ListTile(//添加内容title: Text('hello flutter $i',style: TextStyle(fontSize: 15,color: Colors.red),),subtitle: Text("hello SubTitle $i 00"),leading: Image.network("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=371568753,606618613&fm=26&gp=0.jpg"),trailing: Image.network("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=755344678,1916397087&fm=26&gp=0.jpg"),));}return _list;}

ListView.builder
ListView.builder适合列表项比较多(或者无限)的情况,因为只有当子组件真正显示的时候才会被创建,也就说通过该构造函数创建的ListView是支持基于Sliver的懒加载模型的
构造函数
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, //它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。int itemCount, //列表项的数量,如果为null,则为无限列表。bool addAutomaticKeepAlives = true,bool addRepaintBoundaries = true,bool addSemanticIndexes = true,double cacheExtent,int semanticChildCount,DragStartBehavior dragStartBehavior = DragStartBehavior.start,})
列子:
import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';class SchoolPage extends StatefulWidget {@override_SchoolPageState createState() => _SchoolPageState();}class _SchoolPageState extends State<SchoolPage> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("List learn"),),body: ListView.builder(//iteme 个数itemCount: 50,//每个itemxian显示的内容itemBuilder: (context, index) {return ListTile(title: Text('hello ListView.builder $index'),);},));}}

有没有发现这个和UITableView 基本是一样的.并且他也有复用机制Sliver
ListView.separated
ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。而且 itemBuilder、separatorBuilder、itemCount 都是必选的。
构造函数
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,})
例子1 分割线
import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';class SchoolPage extends StatefulWidget {@override_SchoolPageState createState() => _SchoolPageState();}class _SchoolPageState extends State<SchoolPage> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("List learn"),),body: ListView.separated(//iteme 个数itemCount: 50,//每个itemxian显示的内容itemBuilder: (context, index) {return ListTile(title: Text('hello ListView.builder $index'),leading: Image.network("https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2705929761,3704501509&fm=26&gp=0.jpg"),);},//分割器separatorBuilder: (context,index) => Divider(height: 0.5,color: Colors.orange,),));}}

我们给每个item添加了分割线
例子2 上拉加载更多

import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';import 'package:english_words/english_words.dart';class SchoolPage extends StatefulWidget {@override_SchoolPageState createState() => _SchoolPageState();}class _SchoolPageState extends State<SchoolPage> {static const loadingTag = '##loading##'; //表尾标记var _words = <String>[loadingTag];@overridevoid initState() {// TODO: implement initStatesuper.initState();_retrieveData();}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("List learn"),),body: ListView.separated(//iteme 个数itemCount: _words.length,//每个itemxian显示的内容itemBuilder: (context, index) {//如果到了表尾if (_words[index] == loadingTag) {//不足100天,继续获取数据if (_words.length - 1 < 100) {//获取数据_retrieveData();return Container(padding: EdgeInsets.all(16.0),alignment: Alignment.center,child: SizedBox(width: 24.0,height: 24.0,child: CircularProgressIndicator(strokeWidth: 2.0,),),);}else{return Container(alignment: Alignment.center,padding: EdgeInsets.all(16.0),child: Text("没有更多了",style: TextStyle(fontSize: 16, color: Colors.grey),),);}}//显示itemreturn ListTile(title: Text(_words[index]),trailing: Icon(Icons.accessible),);},//分割器separatorBuilder: (context,index) => Divider(height: 0.5,color: Colors.orange,),));}void _retrieveData() {Future.delayed(Duration(seconds: 2)).then( (e) {_words.insertAll(_words.length - 1,//每次生成20个单词 //需求下载 english_words: ^3.1.0 库generateWordPairs().take(20).map((e) => e.asPascalCase).toList());//重新构建列表setState(() { });});}}
例子3 下拉刷新
import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';import 'package:flutter_app_learn/Toast.dart';class SchoolPage extends StatefulWidget {@override_SchoolPageState createState() => _SchoolPageState();}class _SchoolPageState extends State<SchoolPage> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("List learn"),),body: RefreshIndicator(onRefresh: _onRefresh,child: ListView.separated(//iteme 个数itemCount: 50,//每个itemxian显示的内容itemBuilder: (context, index) {return ListTile(title: Text('hello Refresh Indicator $index'),);},//分割器separatorBuilder: (context,index) => Divider(height: 0.5,color: Colors.orange,),),),);}Future<void> _onRefresh() {return Future.delayed(Duration(seconds: 1), () {Toast.show("已经是最新数据哈~");});}}
其中Toast
- Toast是一个插件.在 pubspec.yaml 文件中 fluttertoast: ^2.1.1
- 新建Toast.dart 文件内容如下: ```dart import ‘package:fluttertoast/fluttertoast.dart’;
class Toast { static show(String msg) { Fluttertoast.showToast(msg: msg); } }
3. 在需要用到的地方导入:```dartimport 'package:english_words/english_words.dart';
- 使用:
Toast.show("已经是最新数据哈~");

例子4 添加header 1
import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';import 'package:flutter_app_learn/Toast.dart';class SchoolPage extends StatefulWidget {@override_SchoolPageState createState() => _SchoolPageState();}class _SchoolPageState extends State<SchoolPage> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("List learn"),),body: Column(children: <Widget>[ListTile(title: Text('Header'),),Divider(height: 0.5, color: Colors.orange,),Expanded(child: RefreshIndicator(onRefresh: _onRefresh,child: ListView.separated(itemBuilder: (context,index) {return ListTile(title: Text('hello Refresh Indicator $index'),);},separatorBuilder: (context,index) => Divider(height: 0.5,color: Colors.orange,),itemCount: 50,),),),],),);}Future<void> _onRefresh() {return Future.delayed(Duration(seconds: 1), () {Toast.show("已经是最新数据哈~");});}}

可以看到这个下拉刷新的位置是在 heder下面的,在布局上我们把刷新组件放到了header的下面,这里用Expanded包了一层. Expanded : 自动拉伸组件大小,也可以按照比例显示
例子5 添加header 2
刷新是在导航栏下面就开始的.
import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';import 'package:flutter_app_learn/Toast.dart';class SchoolPage extends StatefulWidget {@override_SchoolPageState createState() => _SchoolPageState();}class _SchoolPageState extends State<SchoolPage> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("List learn"),),body: RefreshIndicator(onRefresh: _onRefresh,child: Column(children: <Widget>[ListTile(title: Text("Header"),),Divider(height: 0.5, color: Colors.orange,),Expanded(child: ListView.separated(//iteme 个数itemCount: 50,//每个itemxian显示的内容itemBuilder: (context, index) {return ListTile(title: Text('hello Refresh Indicator $index'),);},//分割器separatorBuilder: (context,index) => Divider(height: 0.5,color: Colors.orange,),),),],)),);}Future<void> _onRefresh() {return Future.delayed(Duration(seconds: 1), () {Toast.show("已经是最新数据哈~");});}}

例子6 自定义单元格 cell
1. 造数据
//model类class NewsInfo {final String title;final String subTitle;final String source;final String createTime;final String coverImgUrl;const NewsInfo({this.title,this.subTitle,this.source,this.createTime,this.coverImgUrl,});}
import 'package:flutter_app_learn/news_card.dart';//新闻数据const List<NewsInfo> newsList = [NewsInfo(title: "面对疫情中国终将胜利,任你妖魔鬼怪,中国一定赢! 面对疫情中国终将胜利,任你妖魔鬼怪,中国一定赢! 面对疫情中国终将胜利,任你妖魔鬼怪,中国一定赢!",subTitle: "中国一定赢,中国一定赢,中国一定赢,中国一定赢",source: "纽约时报",createTime: "2020.4.2 18.56",coverImgUrl: "https://goss.cfp.cn/creative/vcg/veer/800/new/VCG41N629382660.jpg"),NewsInfo(title: "面对疫情中国终将胜利,任你妖魔鬼怪,中国一定赢!",subTitle: "中国一定赢,中国一定赢,中国一定赢,中国一定赢",source: "纽约时报",createTime: "2020.4.2 18.56",coverImgUrl: "https://goss4.cfp.cn/creative/vcg/800/new/VCG211255066869.jpg"),NewsInfo(title: "面对疫情中国终将胜利,任你妖魔鬼怪,中国一定赢!",subTitle: "中国一定赢,中国一定赢,中国一定赢,中国一定赢",source: "纽约时报",createTime: "2020.4.2 18.56",coverImgUrl: "https://goss4.cfp.cn/creative/vcg/800/new/VCG41N1096407624.jpg"),..........];
2. 自定义cell
import 'package:flutter/material.dart';class NewsCard extends StatelessWidget {const NewsCard({Key key, this.info}) : super(key: key);//数据源final NewsInfo info;@overrideWidget build(BuildContext context) {return Container(padding: EdgeInsets.all(15),child: Row(children: <Widget>[Expanded(child: Column(children: <Widget>[Text(this.info.title,// maxLines: 2,style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold,color: Color(0xFF333333),),),Padding(padding: EdgeInsets.only(top: 3)),Row(children: <Widget>[Text('${this.info.source} ${this.info.createTime}',style: TextStyle(fontSize: 14,color: Color(0xFfF999999),),)],),],),),Padding(padding: EdgeInsets.only(left: 16),),Image.network(this.info.coverImgUrl,height: 60,width: 100,fit: BoxFit.cover,),],),);}}
3. 加载cell
import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';import 'package:flutter_app_learn/Toast.dart';import 'package:flutter_app_learn/data_soure.dart';import 'package:flutter_app_learn/news_card.dart';class SchoolPage extends StatefulWidget {const SchoolPage({Key key}) : super(key: key);@override_SchoolPageState createState() => _SchoolPageState();}class _SchoolPageState extends State<SchoolPage> {//必须赋值一下,不然报错: Unimplemented handling of missing static targetList list = newsList;@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("List learn"),),body: Column(children: <Widget>[ListTile(title: Text('自定义cell'),),Divider(height: 0.5, color: Colors.orange,),Expanded(child: RefreshIndicator(onRefresh: _onRefresh,child: ListView.separated(itemBuilder: (context,index) => NewsCard(info: this.list[index]),separatorBuilder: (context,index) => Divider(height: 0.5,color: Colors.grey,),itemCount: this.list.length,),),),],),);}Future<void> _onRefresh() {return Future.delayed(Duration(seconds: 1), () {Toast.show("已经是最新数据哈~");});}}
4. 效果图

例子7:点击展开单元格 ExpansionTile
要实现点击展开的效果其实有widget可以使用就是 ExpansionTile
构造方法
const ExpansionTile({Key key,this.leading, //标题左侧要展示的widget@required this.title, //要展示的标题widgetthis.subtitle, //要展示的子标题widgetthis.backgroundColor,this.onExpansionChanged, //列表展开收起的回调函数this.children = const <Widget>[], //列表展开时显示的widgetthis.trailing, //标题右侧要展示的widgetthis.initiallyExpanded = false, ////是否默认状态下展开})
例子
@overrideWidget build(BuildContext context) {return MaterialApp(home: Scaffold(appBar: AppBar(title: Text(''),),body: Column(ExpansionTile(),ExpansionTile(),ExpansionTile()],),),);}}
直接在里面放了三个 空的 ExpansionTile 看下效果:

可以看到,每一行都是单独的ExpansionTile,但我们想实现如下的效果:
具体看代码:
import 'package:flutter/material.dart';void main() => runApp(MyApp());const CITY_NAMES = {'北京': ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区', '顺义区'],'上海': ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '闸北区', '虹口区'],'广州': ['越秀', '海珠', '荔湾', '天河', '白云', '黄埔', '南沙', '番禺'],'深圳': ['南山', '福田', '罗湖', '盐田', '龙岗', '宝安', '龙华'],'杭州': ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区'],'苏州': ['姑苏区', '吴中区', '相城区', '高新区', '虎丘区', '工业园区', '吴江区']};class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {final title = '水平';return MaterialApp(title: title,home: Scaffold(appBar: AppBar(title: Text(title),),body: Container(child: ListView(children: _buildList(),),),),);}List<Widget> _buildList() {List<Widget> widgets = [];CITY_NAMES.keys.forEach((key) {widgets.add(_item(key, CITY_NAMES[key]));});return widgets;}Widget _item(String city, List<String> subCities) {return ExpansionTile(title: Text(city,style: TextStyle(fontSize: 16, color: Colors.black54),),children: subCities.map((subCity) => _buildSub(subCity)).toList(),onExpansionChanged: (value) {print(value);},);}Widget _buildSub(String subCity) {return FractionallySizedBox(widthFactor: 1,child: Container(alignment: Alignment.centerLeft,height: 50,margin: EdgeInsets.only(bottom: 1.5),decoration: BoxDecoration(color: Colors.white),child: Text(subCity,style: TextStyle(fontSize: 15, color: Colors.black38),)),);}}
