原文在这里。
介绍
如果你了解Android或者iOS的开发,你会喜欢Flutter ListView的简洁。本文中,我们就是用几个简单的例子来实现一些很常用的情景。
首先,来看看ListView的几种类型。之后介绍如何处理每个item的style。最后,如何添加和删除item。
准备工作
我(作者)假设你已经把Flutter的开发环境都搭建好了。而且你也对Flutter有基本的了解。如果不是,那么以下的连接可以帮助你:
我在使用的是Android Studio,如果你用的是其他的IDE也OK。
开始
新建一个叫做flutter_listview
的项目。
打开main.dart
文件,使用下面的代码替换掉之前的:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ListViews',
theme: ThemeData(
primarySwatch: Colors.teal,
),
home: Scaffold(
appBar: AppBar(title: Text('ListViews')),
body: BodyLayout(),
),
);
}
}
class BodyLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _myListView(context);
}
}
// replace this function with the code in the examples
Widget _myListView(BuildContext context) {
return ListView();
}
注意最后的_myListView
方法,这里的代码就是我们后面要替换掉的。
ListView的基本类型
静态ListView
如果你有一列数据,而且不会发生太大的更改,那么静态ListView就是最好的选择了。尤其是对于设置这样的页面来说最合适不过。
替换_myListView
的代码:
Widget _myListView(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
title: Text('Sun'),
),
ListTile(
title: Text('Moon'),
),
ListTile(
title: Text('Star'),
),
],
);
}
运行代码,会是这个样子的。(虽然hot reload一般没什么问题,不过偶尔还是需要用hot restart甚至关掉重新运行才行)。
代码的三层关系就是ListView的children
是一个包含了三个ListTile
的数组。ListTile
是定义好的,专门处理ListView的item的布局的。我们上面的例子里面只包含了一个title属性。下面的例子会包含一些样式。
如果要给ListView添加分割线,那么可以使用ListTile.divideTiles
。
Widget _myListView(BuildContext context) {
return ListView(
children: ListTile.divideTiles(
context: context,
tiles: [
ListTile(
title: Text('Sun'),
),
ListTile(
title: Text('Moon'),
),
ListTile(
title: Text('Star'),
),
],
).toList(),
);
}
仔细看,你就会发现分割线已经在了。
动态ListView
静态ListView的所有元素都一起和ListView创建好了。这对于很少数据的处理是可以的。下面就来介绍一下处理很多数据的时候使用的ListView.builder()
。这个方法只会处理要在屏幕上显示的数据,就和Android的RecyclerView
很类似,不过用起来更简单。
使用以下的代码替换_myListView
方法:
Widget _myListView(BuildContext context) {
// backing data
final europeanCountries = ['Albania', 'Andorra', 'Armenia', 'Austria',
'Azerbaijan', 'Belarus', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria',
'Croatia', 'Cyprus', 'Czech Republic', 'Denmark', 'Estonia', 'Finland',
'France', 'Georgia', 'Germany', 'Greece', 'Hungary', 'Iceland', 'Ireland',
'Italy', 'Kazakhstan', 'Kosovo', 'Latvia', 'Liechtenstein', 'Lithuania',
'Luxembourg', 'Macedonia', 'Malta', 'Moldova', 'Monaco', 'Montenegro',
'Netherlands', 'Norway', 'Poland', 'Portugal', 'Romania', 'Russia',
'San Marino', 'Serbia', 'Slovakia', 'Slovenia', 'Spain', 'Sweden',
'Switzerland', 'Turkey', 'Ukraine', 'United Kingdom', 'Vatican City'];
return ListView.builder(
itemCount: europeanCountries.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(europeanCountries[index]),
);
},
);
}
运行之后:
itemCount
会告诉ListView有多少数据要显示,itemBuilder
来动态的处理每一个要显示在ListView上的数据。这个方法的参数context是BuildContext
类型的,另一个参数index
则告诉用户第几个数据要显示在屏幕上了。
无限ListView
很多人都有过在Android或者iOS上构建无限滚动ListView的痛苦经历。Flutter也让这个更加简单。只要删除itemCount
就可以。我们改造一下代码,让每一个ListTile
显示出当前的index值。
Widget _myListView(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Text('row $index'),
);
},
);
}
你可以一直滚动,不会有终点。
如果你要显示分割先,只需要ListView.separated
构造方法。
Widget _myListView(BuildContext context) {
return ListView.separated(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(
title: Text('row $index'),
);
},
separatorBuilder: (context, index) {
return Divider();
},
);
}
ListView里再次显示除了一条模糊不清的分割线。如果要修改的话可以使用Divider
来更改分割线的高度颜色等参数。
横向ListView
也很容易可以新建一个横向滚动的ListView。只需要给定scrollDirection
是横向的。不过还需要搭配一点定制的布局。
Widget _myListView(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 1.0),
color: Colors.tealAccent,
child: Text('$index'),
);
},
);
}
样式
我们上面已经了解了所有的ListView类型。但是都不好看。Flutter提供了很多的选项可以让ListView好看。
定制ListTile
ListTile
基本可以覆盖常规使用的全部定制内容。比如副标题,图片和icon等。
Widget _myListView(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.wb_sunny),
title: Text('Sun'),
),
ListTile(
leading: Icon(Icons.brightness_3),
title: Text('Moon'),
),
ListTile(
leading: Icon(Icons.star),
title: Text('Star'),
),
],
);
}
leading
是用来在ListTile
的开始添加icon或者图片的
对应的还有tailing
属性
ListTile(
leading: Icon(Icons.wb_sunny),
title: Text('Sun'),
trailing: Icon(Icons.keyboard_arrow_right),
),
tailing
的箭头图标让人们以为可以点击。其实还不能点击。我们来看看如何响应用户的点击。也很简单。替换_myListView()
方法的代码:
Widget _myListView(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('assets/sun.jpg'),
),
title: Text('Sun'),
),
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('assets/moon.jpg'),
),
title: Text('Moon'),
),
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('assets/stars.jpg'),
),
title: Text('Star'),
),
],
);
}
现在还不能用,我们先添加一些图片。
这里也可以使用NetworkImage(imageUrl)
代替AssetImage(path)
。暂时先用AssetImage,这样内容都在app里面了。在项目更目录下新建一个assets目录,把下面的图片都加进去。
在pubspec.yaml文件注册这个目录
flutter:
assets:
- assets/
重新运行app(停止了再运行),会看到这样的界面:
最后再来看看副标题:
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('assets/sun.jpg'),
),
title: Text('Sun'),
subtitle: Text('93 million miles away'), // <-- subtitle
),
运行结果:
卡片(Card)
Card是让你的列表看起来酷炫最简单的方法了。只需要让Card包裹ListTile。使用下面的代码替换_myListView
方法
Widget _myListView(BuildContext context) {
final titles = ['bike', 'boat', 'bus', 'car',
'railway', 'run', 'subway', 'transit', 'walk'];
final icons = [Icons.directions_bike, Icons.directions_boat,
Icons.directions_bus, Icons.directions_car, Icons.directions_railway,
Icons.directions_run, Icons.directions_subway, Icons.directions_transit,
Icons.directions_walk];
return ListView.builder(
itemCount: titles.length,
itemBuilder: (context, index) {
return Card( // <-- Card widget
child: ListTile(
leading: Icon(icons[index]),
title: Text(titles[index]),
),
);
},
);
}
你可以修改elevation
属性来修改阴影,也可以试一下shape
和margin
看看有什么效果。
定制列表条目
如果一个ListTile不能满足你的要求,你完全可以定制自己的。ListView需要的只不过是一组组件(widget)。任何组件都可以。我最近处理的每个条目多列的需求可以拿来做一个例子。
Widget _myListView(BuildContext context) {
// the Expanded widget lets the columns share the space
Widget column = Expanded(
child: Column(
// align the text to the left instead of centered
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Title', style: TextStyle(fontSize: 16),),
Text('subtitle'),
],
),
);
return ListView.builder(
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
column,
column,
],
),
),
);
},
);
}
触摸检测
如果你想要ListTile,只需要添加onTap
或者onLongPress
回调。
替换_myListViw
方法代码:
Widget _myListView(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
title: Text('Sun'),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
print('Sun');
},
),
ListTile(
title: Text('Moon'),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
print('Moon');
},
),
ListTile(
title: Text('Star'),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
print('Star');
},
),
],
);
}
有了onTap
方法,我们就可以响应用户的点击了。这里我们print一些字符串。
在实际开发中,更有可能是点击了一行就跳转到别的页面了。可以参考响应用户输入。
如果你也没有使用ListTile,而是使用了自己定制的一套组件。那么最好是做一个重构,比如本利就把他们放在一个InkWell
的定制组件里了。
return ListView.builder(
itemBuilder: (context, index) {
return Card(
child: InkWell(
onTap: () {
print('tapped');
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
column,
column,
],
),
),
),
);
},
);
当然如何重构的选项很多,上栗也不是唯一的标准。
更新数据
添加、删除ListView的行
很容易可以在ListView里更新数据。只需要把ListView放在一个StatefulWidget
里,并在需要更新的时候调用setState
方法。
比如下面的例子里有一个BodyLayout
和_myListViw()
:
class BodyLayout extends StatefulWidget {
@override
BodyLayoutState createState() {
return new BodyLayoutState();
}
}
class BodyLayoutState extends State<BodyLayout> {
List<String> titles = ['Sun', 'Moon', 'Star'];
@override
Widget build(BuildContext context) {
return _myListView();
}
Widget _myListView() {
return ListView.builder(
itemCount: titles.length,
itemBuilder: (context, index) {
final item = titles[index];
return Card(
child: ListTile(
title: Text(item),
onTap: () { // <-- onTap
setState(() {
titles.insert(index, 'Planet');
});
},
onLongPress: () { // <-- onLongPress
setState(() {
titles.removeAt(index);
});
},
),
);
},
);
}
}
点击一行,就在那一行的index上添加一行,长按就删除一行。
在AnimatedList里添加、删除行
把BodyLayoutState
的代码替换为下面的内容:
class BodyLayoutState extends State<BodyLayout> {
// The GlobalKey keeps track of the visible state of the list items
// while they are being animated.
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
// backing data
List<String> _data = ['Sun', 'Moon', 'Star'];
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(
height: 300,
child: AnimatedList(
// Give the Animated list the global key
key: _listKey,
initialItemCount: _data.length,
// Similar to ListView itemBuilder, but AnimatedList has
// an additional animation parameter.
itemBuilder: (context, index, animation) {
// Breaking the row widget out as a method so that we can
// share it with the _removeSingleItem() method.
return _buildItem(_data[index], animation);
},
),
),
RaisedButton(
child: Text('Insert item', style: TextStyle(fontSize: 20)),
onPressed: () {
_insertSingleItem();
},
),
RaisedButton(
child: Text('Remove item', style: TextStyle(fontSize: 20)),
onPressed: () {
_removeSingleItem();
},
)
],
);
}
// This is the animated row with the Card.
Widget _buildItem(String item, Animation animation) {
return SizeTransition(
sizeFactor: animation,
child: Card(
child: ListTile(
title: Text(
item,
style: TextStyle(fontSize: 20),
),
),
),
);
}
void _insertSingleItem() {
String newItem = "Planet";
// Arbitrary location for demonstration purposes
int insertIndex = 2;
// Add the item to the data list.
_data.insert(insertIndex, newItem);
// Add the item visually to the AnimatedList.
_listKey.currentState.insertItem(insertIndex);
}
void _removeSingleItem() {
int removeIndex = 2;
// Remove item from data list but keep copy to give to the animation.
String removedItem = _data.removeAt(removeIndex);
// This builder is just for showing the row while it is still
// animating away. The item is already gone from the data list.
AnimatedListRemovedItemBuilder builder = (context, animation) {
return _buildItem(removedItem, animation);
};
// Remove the item visually from the AnimatedList.
_listKey.currentState.removeItem(removeIndex, builder);
}
}
在代码的注释中添加了很多说明。可以总结为一下几点
- AnimatedList需要用到
GlobalKey
。每次动画的时候都需要更新AnimatedList用到的数据和GlobalKey。 - 行组件是stateless的。如果是有状态的,那么就需要安排一个Key给他们。这样可以让Flutter快速的发现哪里发生了更新。这个来自Flutter团队的视频可以帮你了解更多。
- 本例我是用了
SizedTransition
动画,文档里还有更多的可以用。
最后
我们已经了解了ListView的方方面面。你已经可以自己写一个满足自己需要的了。
代码在这里。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。