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,列表滚动至边缘后继续拖动的物理效果,
Android
与iOS
效果不同。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.build
或ListView.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.custom
的SliverChildDelegate
构造,它提供了定制子模型的其他方面的能力。
下面我们分别使用其他三种方法来创建一下
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 属性,反而多了 itemBuilder
和 itemCount
两个属性:
- 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(手势检测)
来实现的,可以看这里 延伸知识点:手势处理