原文在這裏。git
若是你瞭解Android或者iOS的開發,你會喜歡Flutter ListView的簡潔。本文中,咱們就是用幾個簡單的例子來實現一些很經常使用的情景。github
首先,來看看ListView的幾種類型。以後介紹如何處理每一個item的style。最後,如何添加和刪除item。數組
我(做者)假設你已經把Flutter的開發環境都搭建好了。並且你也對Flutter有基本的瞭解。若是不是,那麼如下的鏈接能夠幫助你:app
我在使用的是Android Studio,若是你用的是其餘的IDE也OK。less
新建一個叫作flutter_listview
的項目。dom
打開main.dart
文件,使用下面的代碼替換掉以前的:ide
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就是最好的選擇了。尤爲是對於設置這樣的頁面來講最合適不過。動畫
替換_myListView
的代碼:ui
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的方方面面。你已經能夠本身寫一個知足本身須要的了。
代碼在這裏。