Flutter 中的ListView
是最常用的可滾動的元件之一,
它可以沿一個方向線性排布所有子元件,並且它也支援基於Sliver的延遲構建模型。
基於Sliver的延遲構建
什麼是基於Sliver的延遲構建模型呢?
通常可滾動元件的子元件可能會非常多、佔用的總高度也會非常大;如果要一次性將子元件全部構建出將會非常昂貴!為此,Flutter中提出一個Sliver(中文為“薄片”的意思)概念,如果一個可滾動元件支援Sliver模型,那麼該滾動可以將子元件分成好多個“薄片”(Sliver),只有當Sliver出現在視口中時才會去構建它,這種模型也稱為“基於Sliver的延遲構建模型”。
原始碼示例
建構函式如下:
ListView({
...
//可滾動元件的公共引數
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
EdgeInsetsGeometry padding,
//ListView各個建構函式的共同引數
this.itemExtent,
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
//子元件列表
List<Widget> children = const <Widget>[],
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
})
複製程式碼
屬性解釋
scrollDirection
決定子元件的滾動方向(排列方向),預設是垂直方向
scrollDirection:Axis.horizontal,
水平方向
scrollDirection:Axis.vertical,
垂直方向
reverse
決定滾動方向是否與閱讀方向一致
primary
當內容不足以滾動時,是否支援滾動;
值為true
或者false
,我試了一下,好像沒什麼卵用,不知道是理解錯了,還是怎麼的,先寫上吧
controller
此屬性接收一個ScrollController
物件。ScrollController
的主要作用是控制滾動位置和監聽滾動事件。
有關ScrollController
的使用及詳情,請參考Flutter 滾動控制元件篇-->滾動監聽及控制(ScrollController)
預設情況下,Widget樹中會有一個預設的PrimaryScrollController
,如果子樹中的可滾動元件沒有顯式的指定controller
,並且primary
屬性值為true
時(預設就為true
),可滾動元件會使用這個預設的PrimaryScrollController
。這種機制帶來的好處是父元件可以控制子樹中可滾動元件的滾動行為,
physics
此屬性接受一個ScrollPhysics
型別的物件,它決定可滾動元件如何響應使用者操作,比如使用者滑動完抬起手指後,繼續執行動畫;或者滑動到邊界時,如何顯示。
在iOS上會出現彈性效果,而在Android上會出現微光效果。
shrinkWrap
該屬性表示是否根據子元件的總長度來設定ListView
的長度,預設值為false
。
預設情況下,ListView
會在滾動方向儘可能多的佔用空間。當ListView
在一個無邊界(滾動方向上)的容器中時,shrinkWrap
必須為true
,否則會報錯。
itemExtent
該引數如果不為null
,則會強制children
的“長度”為itemExtent
的值;
這裡的“長度”是指滾動方向上子元件的長度,也就是說如果滾動方向是垂直方向,則itemExtent
代表子元件的高度;如果滾動方向為水平方向,則itemExtent
就代表子元件的寬度。
addAutomaticKeepAlives
該屬性表示是否將列表項(子元件)包裹在AutomaticKeepAlive
元件中;
在一個懶載入列表中,如果將列表項包裹在AutomaticKeepAlive
中,在該列表項滑出視口時也不會被回收,它會使用KeepAliveNotification
來儲存其狀態。如果列表項自己維護其KeepAlive
狀態,那麼此引數必須置為false
。
addRepaintBoundaries
該屬性表示是否將列表項(子元件)包裹在RepaintBoundary
元件中。
當可滾動元件滾動時,將列表項包裹在RepaintBoundary
中可以避免列表項重繪,但是當列表項重繪的開銷非常小(如一個顏色塊,或者一個較短的文字)時,不新增RepaintBoundary
反而會更高效。和addAutomaticKeepAlive
一樣,如果列表項自己維護其KeepAlive
狀態,那麼此引數必須置為false
。
addSemanticIndexes
該屬性表示是否把子控制元件包裝在IndexedSemantics
裡,用來提供無障礙語義
cacheExtent
可見區域的前後會有一定高度的空間去快取子控制元件,當滑動時就可以迅速呈現
簡單的就是說,當你快要滑到載入資料的時候,他已經提前一步載入好了,等到你滑到的時候就會顯示出來,而不至於使用者滑到的時候還需要等待一會兒。
semanticChildCount
有含義的子控制元件的數量
如:ListView
會用children
的長度,而ListView.separated
會用children
長度的一半
children
這裡的children
需要說一下,和別的元件裡的不一樣。
這裡的children
引數,他就收一個列表,但是這種方式適合只有少量的子元件的情況。因為這種方式需要將所有children
都提前建立好(這需要做大量工作),而不是等到子元件真正顯示的時候再建立,也就是說通過預設建構函式構建的ListView
沒有應用基於Sliver的懶載入模型。
再次強調,可滾動元件通過一個List
來作為其children
屬性時,只適用於子元件較少的情況,這是一個通用規律。
ListView.builder
上面的children
只適合資料較少的情況下使用
而ListView.builder
則適合列表項比較多(或者無限)的情況下使用,因為只有當子元件真正顯示的時候才會被建立,也就說通過該建構函式建立的ListView
是支援基於Sliver的懶載入模型的。
原始碼示例
建構函式如下:
ListView.builder({
// ListView公共引數已省略
...
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
...
})
複製程式碼
屬性解釋
itemBuilder
它是列表項的構建器,型別為IndexedWidgetBuilder
,返回值為一個widget(就是一個元件)。當列表滾動到具體的index
位置時,會呼叫該構建器構建列表項,也就是所謂的基於Sliver的懶載入模型。
itemCount
該屬性表示列表項的數量,如果為null
,則表示無限列表
注:可滾動元件的建構函式如果需要一個列表項Builder,那麼通過該建構函式構建的可滾動元件通常就是支援基於Sliver的懶載入模型的,反之則不支援,這是個一般規律。
程式碼示例:
ListView.builder(
itemCount: 100,
itemExtent: 50.0, //強制高度為50.0,如果這個值越來越小的話,那麼顯示的值是會重疊的
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
})
複製程式碼
執行效果:
ListView.separated
ListView.separated
可以在生成的列表項之間新增一個分割元件。
它比ListView.builder
多了一個separatorBuilder
引數,該引數是一個分割元件生成器。
程式碼示例:
在奇數行新增一條藍色下劃線,偶數行新增一條紅色下劃線。
import 'package:flutter/material.dart';
class CategoryPage extends StatefulWidget {
@override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
@override
Widget build(BuildContext context) {
//下劃線widget預定義以供複用。
Widget Lineblue = Divider(color: Colors.blue);
Widget Linered = Divider(color: Colors.red);
return Scaffold(
appBar: AppBar(
title: Text(
"ListView.separated",
// style: TextStyle(color: Color(0xFF1E88E5)),
),
),
body: ListView.separated(
itemCount: 100,
//列表項構造器
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
//分割器構造器
separatorBuilder: (BuildContext context, int index) {
return index % 2 == 0 ? Lineblue : Linered;
},
));
}
}
複製程式碼
執行效果:
無限載入列表
假設我們要從資料來源非同步分批拉取一些資料,然後用ListView
展示。
當我們滑動到列表末尾時,判斷是否需要再去拉取資料,如果是,則去拉取,拉取過程中在表尾顯示一個轉著的小圓圈,拉取成功後將資料插入列表;如果不需要再去拉取,則在表尾提示"沒有更多了"。
這裡我們需要安裝一個包english_words: ^3.1.5
(在pubspec.yaml
檔案中的dependencies
下安裝),可以給我們自動的生成英語單詞
程式碼示例:
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
class CategoryPage extends StatefulWidget {
@override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
static const loadingTag = "##loading##"; //表尾標記
var _words = <String>[loadingTag];
@override
void initState() {
super.initState();
_retrieveData();
}
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: _words.length,
itemBuilder: (context, index) {
//如果到了表尾
if (_words[index] == loadingTag) {
//不足100條,繼續獲取資料
if (_words.length - 1 < 100) {
//獲取資料
_retrieveData();
//載入時顯示loading
return Container(
padding: const EdgeInsets.all(16.0),
alignment: Alignment.center,
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(strokeWidth: 2.0)),
);
} else {
//已經載入了100條資料,不再獲取資料。
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16.0),
child: Text(
"沒有更多了",
style: TextStyle(color: Colors.grey),
));
}
}
//顯示單詞列表項
return ListTile(title: Text(_words[index]));
},
separatorBuilder: (context, index) => Divider(height: .0),
);
}
void _retrieveData() {
Future.delayed(Duration(seconds: 2)).then((e) {
_words.insertAll(
_words.length - 1,
//每次生成20個單詞
generateWordPairs().take(20).map((e) => e.asPascalCase).toList());
setState(() {
//重新構建列表
});
});
}
}
複製程式碼
_retrieveData()
的功能是模擬從資料來源非同步獲取資料,
english_words
包的generateWordPairs()
方法可以每次生成20個單詞。
執行效果:
ListTile
這裡我們說一下ListTile
。
ListTile
通常用於在 Flutter 中填充 ListView
。
原始碼示例:
建構函式如下:
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,
})
複製程式碼
屬性解釋
title
title
引數可以接受任何小元件,但通常是文字小元件
程式碼示例:
ListTile(
title: Text('我喜歡你!'),
)
複製程式碼
subtitle
他是一個副標題,顯示在標題(title)下面較小的文字
程式碼示例:
ListTile(
title: Text('我喜歡你!'),
subtitle: Text('你喜歡我嗎?'),
)
複製程式碼
dense
使文字更小,並將所有內容打包在一起
程式碼示例:
ListTile(
title: Text('我喜歡你!'),
subtitle: Text('你喜歡我嗎?'),
dense:true,
)
複製程式碼
leading
將影象或圖示新增到列表的開頭。
程式碼示例:
ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(imageUrl),
),
title: Text('我喜歡你'),
subtitle: Text('你喜歡我嗎?'),
dense:true,
)
複製程式碼
trailing
在列表的末尾放置一個影象。
程式碼示例:
ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(imageUrl),
),
title: Text('我喜歡你'),
subtitle: Text('你喜歡我嗎?'),
dense:true,
trailing: Icon(Icons.keyboard_arrow_right),
)
複製程式碼
contentPadding
設定內容邊距,預設是 16
我這裡設定30
程式碼示例:
ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(imageUrl),
),
title: Text('我喜歡你'),
subtitle: Text('你喜歡我嗎?'),
dense:true,
trailing: Icon(Icons.keyboard_arrow_right),
contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
)
複製程式碼
selected
如果選中列表的 item
項,那麼文字和圖示的顏色將成為主題的主顏色。
程式碼示例:
ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(imageUrl),
),
title: Text('我喜歡你'),
subtitle: Text('你喜歡我嗎?'),
dense:true,
trailing: Icon(Icons.keyboard_arrow_right),
contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
selected: true,
)
複製程式碼
onTap、onLongPress
onTap
為單擊,onLongPress
為長按。
程式碼示例:
ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(imageUrl),
),
title: Text('我喜歡你'),
subtitle: Text('你喜歡我嗎?'),
dense:true,
trailing: Icon(Icons.keyboard_arrow_right),
contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
selected: true,
onTap: () {
// do something
},
onLongPress: (){
// do something else
},
)
複製程式碼
enabled
通過將 enable
設定為 false
,來禁止點選事件
這裡就不寫程式碼了,比較簡單。
屬性demo示例
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
class CategoryPage extends StatefulWidget {
@override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ListTile'),),
body: Column(
children: <Widget>[
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('images/Test.jpg'),
),
title: Text('我喜歡你'),
subtitle: Text('你喜歡我嗎?'),
dense: true,
trailing: Icon(Icons.keyboard_arrow_right),
contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
// selected: true,
onTap: () {
// do something
},
onLongPress: () {
// do something else
},
),
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('images/Test.jpg'),
),
title: Text('我喜歡你'),
subtitle: Text('你喜歡我嗎?'),
dense: true,
trailing: Icon(Icons.keyboard_arrow_right),
contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
selected: true,
onTap: () {
// do something
},
onLongPress: () {
// do something else
},
)
],
));
}
}
複製程式碼
執行效果:
新增固定列表頭
很多時候我們需要給列表新增一個固定表頭。
我們需要讓ListView
自動拉伸以適應螢幕,這個時候就需要我們使用到彈性佈局Flex
,如果不知道的話,請移步Flutter 佈局控制元件篇-->Flex、Expanded
我們可以使用Expanded
自動拉伸元件大小,並且我們也說過Column
是繼承自Flex
的,所以我們可以直接使用Column+Expanded
來實現,
程式碼示例:
Column(children: <Widget>[
ListTile(title: Text("數字列表")),
Expanded(
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
),
]);
複製程式碼
執行效果: