原文在這裡。
介紹
如果你瞭解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的方方面面。你已經可以自己寫一個滿足自己需要的了。
程式碼在這裡。