原文在这里。git
若是你了解Android或者iOS的开发,你会喜欢Flutter ListView的简洁。本文中,咱们就是用几个简单的例子来实现一些很经常使用的情景。github
首先,来看看ListView的几种类型。以后介绍如何处理每一个item的style。最后,如何添加和删除item。数组
我(做者)假设你已经把Flutter的开发环境都搭建好了。并且你也对Flutter有基本的了解。若是不是,那么如下的链接能够帮助你:markdown
我在使用的是Android Studio,若是你用的是其余的IDE也OK。app
新建一个叫作flutter_listview
的项目。less
打开main.dart
文件,使用下面的代码替换掉以前的:dom
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
方法,这里的代码就是咱们后面要替换掉的。ide
若是你有一列数据,并且不会发生太大的更改,那么静态ListView就是最好的选择了。尤为是对于设置这样的页面来讲最合适不过。oop
替换_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.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
则告诉用户第几个数据要显示在屏幕上了。
不少人都有过在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。只须要给定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
基本能够覆盖常规使用的所有定制内容。好比副标题,图片和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包裹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放在一个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上添加一行,长按就删除一行。
把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); } } 复制代码
在代码的注释中添加了不少说明。能够总结为一下几点
GlobalKey
。每次动画的时候都须要更新AnimatedList用到的数据和GlobalKey。SizedTransition
动画,文档里还有更多的能够用。咱们已经了解了ListView的方方面面。你已经能够本身写一个知足本身须要的了。
代码在这里。