- Flutter和Dart系列文章和程式碼GitHub地址
- 在
Flutter
中, 當內容超過顯示檢視時,如果沒有特殊處理,Flutter
則會提示Overflow
錯誤 Flutter
提供了多種可滾動(Scrollable Widget
)用於顯示列表和長佈局- 可滾動
Widget
都直接或間接包含一個Scrollable
, 下面是常用的幾個可滾動的Widget
SingleChildScrollView
ListView
GridView
CustomScrollView
- 滾動監聽及控制
ScrollController
Scrollbar
Scrollbar
是一個Material
風格的滾動指示器(滾動條),如果要給可滾動widget
新增滾動條,只需將Scrollbar
作為可滾動widget
的父widget
即可CupertinoScrollbar
是iOS
風格的滾動條,如果你使用的是Scrollbar
,那麼在iOS
平臺它會自動切換為CupertinoScrollbar
Scrollbar
和CupertinoScrollbar
都是通過ScrollController
來監聽滾動事件來確定滾動條位置,關於ScrollController
詳細的內容我們將在後面專門一節介紹- 下面是
Scrollbar
和CupertinoScrollbar
的建構函式, 都只有一個child
屬性, 用於接受一個可滾動的Widget
const Scrollbar({
Key key,
@required this.child,
})
const CupertinoScrollbar({
Key key,
@required this.child,
})
複製程式碼
主軸和縱軸
- 在可滾動
widget
的座標描述中,通常將滾動方向稱為主軸,非滾動方向稱為縱軸。 - 由於可滾動
widget
的預設方向一般都是沿垂直方向,所以預設情況下主軸就是指垂直方向,水平方向同理
SingleChildScrollView
SingleChildScrollView
類似於開發中常用的ScrollView
, 不再詳細介紹了, 下面看一下具體使用介紹吧
const SingleChildScrollView({
Key key,
// 設定滾動的方向, 預設垂直方向
this.scrollDirection = Axis.vertical,
// 設定顯示方式
this.reverse = false,
// 內邊距
this.padding,
// 是否使用預設的controller
bool primary,
// 設定可滾動Widget如何響應使用者操作
this.physics,
this.controller,
this.child,
})
複製程式碼
scrollDirection
設定檢視的滾動方向(預設垂直方向), 需要對應的設定其子Widget
是Column
或者Row
, 否則會報Overflow
錯誤
scrollDirection: Axis.vertical,
// 列舉值
enum Axis {
/// 水平滾動
horizontal,
/// 垂直滾動
vertical,
}
複製程式碼
reverse
- 是否按照閱讀方向相反的方向滑動
- 設定水平滾動時
- 若
reverse: false
,則滾動內容頭部和左側對其, 那麼滑動方向就是從左向右 reverse: true
時,則滾動內容尾部和右側對其, 那麼滑動方向就是從右往左。
- 若
- 其實此屬性本質上是決定可滾動
widget
的初始滾動位置是在頭還是尾,取false
時,初始滾動位置在頭,反之則在尾
physics
- 此屬性接受一個
ScrollPhysics
物件,它決定可滾動Widget
如何響應使用者操作 - 比如使用者滑動完抬起手指後,繼續執行動畫;或者滑動到邊界時,如何顯示。
- 預設情況下,
Flutter
會根據具體平臺分別使用不同的ScrollPhysics
物件,應用不同的顯示效果,如當滑動到邊界時,繼續拖動的話,在iOS
上會出現彈性效果,而在Android
上會出現微光效果。 - 如果你想在所有平臺下使用同一種效果,可以顯式指定,
Flutter SDK
中包含了兩個ScrollPhysics
的子類可以直接使用:ClampingScrollPhysics
:安卓下微光效果。BouncingScrollPhysics
:iOS
下彈性效果。
controller
- 此屬性接受一個
ScrollController
物件 ScrollController
的主要作用是控制滾動位置和監聽滾動事件。- 預設情況下,
widget
中會有一個預設的PrimaryScrollController
,如果子widget
中的可滾動widget
沒有顯式的指定controller
並且primary
屬性值為true
時(預設就為true
),可滾動widget
會使用這個預設的PrimaryScrollController
- 這種機制帶來的好處是父
widget
可以控制子樹中可滾動widget
的滾動,例如,Scaffold
使用這種機制在iOS
中實現了"回到頂部"的手勢
程式碼示例
class ScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return Scrollbar(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
reverse: true,
padding: EdgeInsets.all(0.0),
physics: BouncingScrollPhysics(),
child: Center(
child: Column(
//動態建立一個List<Widget>
children: str.split("")
//每一個字母都用一個Text顯示,字型為原來的兩倍
.map((c) => Text(c, textScaleFactor: 2.0))
.toList(),
),
),
),
);
}
}
複製程式碼
ListView
ListView
是最常用的可滾動widget
,它可以沿一個方向線性排布所有子widget
, 類似於ReactNative
中的ListView
ListView
共有四種建構函式ListView()
預設建構函式ListView.builder()
ListView.separated()
ListView custom()
ListView({
// 公共引數上面都介紹過了
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
EdgeInsetsGeometry padding,
// 是否根據子widget的總長度來設定ListView的長度,預設值為false
bool shrinkWrap = false,
// cell高度
this.itemExtent,
// 子widget是否包裹在AutomaticKeepAlive中
bool addAutomaticKeepAlives = true,
// 子widget是否包裹在RepaintBoundary中
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
// 設定預載入的區域, moren 0.0
double cacheExtent,
//子widget列表
List<Widget> children = const <Widget>[],
// 子widget的個數
int semanticChildCount,
})
複製程式碼
屬性介紹
shrinkWrap
- 表示是否根據子
widget
的總長度來設定ListView
的長度,預設值為false
。 - 預設情況下,
ListView
的會在滾動方向儘可能多的佔用空間 - 當
ListView
在一個無邊界(滾動方向上)的容器中時,shrinkWrap
必須為true
itemExtent
- 該引數如果不為
null
,則會強制children
的"長度"為itemExtent
的值 - 這裡的"長度"是指滾動方向上子
widget
的長度,即如果滾動方向是垂直方向,則代表子widget
的高度,如果滾動方向為水平方向,則代表子widget
的長度 - 在
ListView
中,指定itemExtent
比讓子widget
自己決定自身長度會更高效,這是因為指定itemExtent
後,滾動系統可以提前知道列表的長度,而不是總是動態去計算,尤其是在滾動位置頻繁變化時
addAutomaticKeepAlives
- 表示是否將列表項包裹在
AutomaticKeepAlive
中 - 在一個懶載入列表中,如果將列表項包裹在
AutomaticKeepAlive
中,在該列表項滑出視口時該列表項不會被GC,它會使用KeepAliveNotification
來儲存其狀態 - 如果列表項自己維護其
KeepAlive
狀態,那麼此引數必須置為false
addRepaintBoundaries
- 性表示是否將列表項包裹在
RepaintBoundary
中 - 當可滾動
widget
滾動時,將列表項包裹在RepaintBoundary
中可以避免列表項重繪,但是當列表項重繪的開銷非常小(如一個顏色塊,或者一個較短的文字)時,不新增RepaintBoundary
反而會更高效 - 和
addAutomaticKeepAlive
一樣,如果列表項自己維護其KeepAlive
狀態,那麼此引數必須置為false
使用示例
class ScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
itemExtent: 60,
cacheExtent: 100,
addAutomaticKeepAlives: false,
children: renderCell(),
);
}
List<Widget> renderCell() {
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return str.split("")
.map((item) => ListTile(
title: Text('字母--$item'),
subtitle: Text('這是字母列表'),
leading: Icon(Icons.wifi),
)).toList();
}
}
複製程式碼
ListTile
ListTile
是Flutter
給我們準備好的用於建立ListView
的子widget
- 提供非常常見的構造和定義方式,包括文字,icon,點選事件,一般是能夠滿足基本需求,但是就不能自己定義了
const ListTile({
Key key,
// 前置(左側)圖示, Widget型別
this.leading,
// 標題, Widget型別
this.title,
// 副標題, Widget型別
this.subtitle,
// 後置(右側)圖示, Widget型別
this.trailing,
// 是否三行顯示, subtitle不為空時才能使用
this.isThreeLine = false,
// 設定為true後字型變小
this.dense,
// 內容的內邊距
this.contentPadding,
// 是否可被點選
this.enabled = true,
// 點選事件
this.onTap,
// 長按操作事件
this.onLongPress,
// 是否是選中狀態
this.selected = false,
})
// 使用示例
return ListTile(
title: Text('index--$index'),
subtitle: Text('我是一隻小鴨子, 咿呀咿呀喲; 我是一隻小鴨子, 咿呀咿呀喲; 我是一隻小鴨子, 咿呀咿呀喲;'),
leading: Icon(Icons.wifi),
trailing: Icon(Icons.keyboard_arrow_right),
isThreeLine: true,
dense: false,
contentPadding: EdgeInsets.all(10),
enabled: index % 3 != 0,
onTap: () => print('index = $index'),
onLongPress: () => print('long-Index = $index'),
selected: index % 2 == 0,
);
複製程式碼
ListView.builder
ListView.builder
適合列表項比較多(或者無限)的情況,因為只有當子Widget
真正顯示的時候才會被建立- 適用於自定義子
Widget
且所有子Widget
的樣式一樣
ListView.builder({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
//
@required IndexedWidgetBuilder itemBuilder,
// 列表項的數量,如果為null,則為無限列表
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
})
複製程式碼
itemCount
列表項的數量,如果為null,則為無限列表
itemBuilder
- 它是列表項的構建器,型別為
IndexedWidgetBuilder
,返回值為一個widget
- 當列表滾動到具體的
index
位置時,會呼叫該構建器構建列表項
程式碼示例
class ListBuild extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 30,
itemBuilder: (content, index) {
return ListTile(
title: Text('index--$index'),
subtitle: Text('數字列表'),
leading: Icon(Icons.wifi),
);
},
);
}
}
複製程式碼
ListView.separated
ListView.separated
可以生成列表項之間的分割器,它除了比ListView.builder
多了一個separatorBuilder
引數外, 其他引數都一樣
ListView.separated({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required IndexedWidgetBuilder itemBuilder,
// 一個分割生成器
@required IndexedWidgetBuilder separatorBuilder,
@required int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
})
複製程式碼
separatorBuilder
該引數是一個分割生成器, 同樣是一個IndexedWidgetBuilder
型別的引數
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
複製程式碼
程式碼示例
奇數行新增一條紅色下劃線,偶數行新增一條藍色下劃線。
lass SeparatedList extends StatelessWidget {
//下劃線widget預定義以供複用。
Widget lineView1 = Divider(color: Colors.red, height: 2, indent: 10,);
Widget lineView2 = Divider(color: Colors.blue, height: 5, indent: 30);
@override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.separated(
itemCount: 30,
itemBuilder: (content, index) {
return ListTile(
title: Text('index--$index'),
subtitle: Text('數字列表'),
leading: Icon(Icons.wifi),
);
},
separatorBuilder: (context, index) {
return index % 2 == 0 ? lineView1 : lineView2;
},
);
}
}
複製程式碼
Divider
設定每一個子WIdget
的分割線
const Divider({
Key key,
// 分割線所在的SizedBox的高度, 除內邊距之外的距離上面的間距
this.height = 16.0,
// 分割線左側間距
this.indent = 0.0,
// 分割線顏色
this.color
})
複製程式碼
ListView.custom
- 大家可能對前兩種比較熟悉,分別是傳入一個子元素列表或是傳入一個根據索引建立子元素的函式。
- 其實前兩種方式都是
custom
方式的“快捷方式” ListView
內部是靠這個childrenDelegate
屬性動態初始化子元素的- 我們使用
builder
和separated
比較多,這個custom
相對來說就比較少了
const ListView.custom({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
// 動態初始化子元素
@required this.childrenDelegate,
double cacheExtent,
int semanticChildCount,
})
複製程式碼
childrenDelegate
其實在ListView
的前面幾種建構函式中, 都預設設定了childrenDelegate
這個屬性, 更多可參考官方文件
// ListView
ListView({
// ...
}) : childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super();
// ListView.builder
ListView.builder({
// ...
}) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super();
// ListView.separated
ListView.separated({
// ...
}) : childrenDelegate = SliverChildBuilderDelegate(
// ...
), super();
複製程式碼
- 上面程式碼中可見,這裡自動幫我們建立了一個
SliverChildListDelegate
的例項 - 而
SliverChildListDelegate
是抽象類SliverChildDelegate
的子類 SliverChildListDelegate
中主要邏輯就是實現了SliverChildDelegate
中定義的build
方法
Widget build(BuildContext context, int index) {
assert(builder != null);
if (index < 0 || (childCount != null && index >= childCount))
return null;
Widget child;
try {
child = builder(context, index);
} catch (exception, stackTrace) {
child = _createErrorWidget(exception, stackTrace);
}
if (child == null)
return null;
if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index);
if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null)
child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
}
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return child;
}
複製程式碼
- 從上面程式碼的邏輯可以看出, 就是根據傳入的索引返回
children
列表中對應的元素 - 每當
ListView
的底層實現需要載入一個元素時,就會把該元素的索引傳遞給SliverChildDelegate
的build
方法,由該方法返回具體的元素 - 另外在
SliverChildDelegate
內部,除了定義了build
方法外,還定義了 一個名為didFinishLayout
的方法
void didFinishLayout() {
assert(debugAssertChildListLocked());
final int firstIndex = _childElements.firstKey() ?? 0;
final int lastIndex = _childElements.lastKey() ?? 0;
widget.delegate.didFinishLayout(firstIndex, lastIndex);
}
複製程式碼
- 每當
ListView
完成一次layout
之後都會呼叫該方法, 同時傳入兩個索引值 - 這兩個值分別是此次
layout
中第一個元素和最後一個元素在ListView
所有子元素中的索引值, 也就是可視區域內的元素在子元素列表中的位置 - 然而不論是
SliverChildListDelegate
還是SliverChildBuilderDelegate
的程式碼中,都沒有didFinishLayout
的具體實現。所以我們需要編寫一個它們的子類
class MySliverBuilderDelegate extends SliverChildBuilderDelegate {
MySliverBuilderDelegate(
Widget Function(BuildContext, int) builder, {
int childCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
}) : super(builder,
childCount: childCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries);
@override
void didFinishLayout(int firstIndex, int lastIndex) {
print('firstIndex: $firstIndex, lastIndex: $lastIndex');
}
}
複製程式碼
然後我們建立一個ListView.custom
的列表檢視
class CustomList extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.custom(
childrenDelegate: MySliverBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('index--$index'),
subtitle: Text('數字列表'),
leading: Icon(Icons.wifi),
);
}, childCount: 30,
),
);
}
}
複製程式碼
GridView
GridView
可以構建二維網格列表, 系統給出了五中建構函式
GridView()
GridView.count
GridView.extent
GridView.builder
GridView.custom
// 預設建構函式
GridView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required this.gridDelegate,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
})
複製程式碼
- 可以看到, 除了
gridDelegate
屬性外, 其他屬性和ListView
的屬性都一樣, 含義也都相同 gridDelegate
引數的型別是SliverGridDelegate
,它的作用是控制GridView
子widget
如何排列SliverGridDelegate
是一個抽象類,定義了GridView
排列相關介面,子類需要通過實現它們來實現具體的佈局演算法Flutter
中提供了兩個SliverGridDelegate
的子類SliverGridDelegateWithFixedCrossAxisCount
和SliverGridDelegateWithMaxCrossAxisExtent
, 下面我們分別介紹
SliverGridDelegateWithFixedCrossAxisCount
該子類實現了一個橫軸為固定數量子元素的排列演算法,其建構函式為:
const SliverGridDelegateWithFixedCrossAxisCount({
// 橫軸子元素的數量,此屬性值確定後子元素在橫軸的長度就確定了,即ViewPort橫軸長度/crossAxisCount。
@required this.crossAxisCount,
// 主軸方向的間距
this.mainAxisSpacing = 0.0,
// 側軸方向子元素的間距
this.crossAxisSpacing = 0.0,
// 子元素在側軸長度和主軸長度的比例, 由於crossAxisCount指定後子元素橫軸長度就確定了,然後通過此引數值就可以確定子元素在主軸的長度
this.childAspectRatio = 1.0,
})
複製程式碼
從上面的個屬性可以發現,子元素的大小是通過crossAxisCount
和childAspectRatio
兩個引數共同決定的。注意,這裡的子元素指的是子widget
的最大顯示空間,注意確保子widget
的實際大小不要超出子元素的空間, 程式碼示例如下
class ScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView(
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1,
mainAxisSpacing: 10,
crossAxisSpacing: 10
),
children: <Widget>[
Container(color: Colors.orange),
Container(color: Colors.blue),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink)
],
);
}
}
複製程式碼
GridView.count
GridView.count
建構函式內部使用了SliverGridDelegateWithFixedCrossAxisCount
,我們通過它可以快速的建立橫軸固定數量子元素的GridView
GridView.count({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required int crossAxisCount,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
})
複製程式碼
上面SliverGridDelegateWithFixedCrossAxisCount
中給出的示例程式碼等價於:
class CountGridView extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return GridView.count(
padding: EdgeInsets.all(10),
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1,
children: <Widget>[
Container(color: Colors.orange),
Container(color: Colors.blue),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink)
],
);
}
}
複製程式碼
SliverGridDelegateWithMaxCrossAxisExtent
該子類實現了一個側軸子元素為固定最大長度的排列演算法,其建構函式為:
const SliverGridDelegateWithMaxCrossAxisExtent({
@required this.maxCrossAxisExtent,
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
})
複製程式碼
maxCrossAxisExtent
為子元素在側軸上的最大長度,之所以是“最大”長度,是因為橫軸方向每個子元素的長度仍然是等分的- 同樣側軸上子
Widget
的個數, 也是由該屬性決定 - 其它引數和
SliverGridDelegateWithFixedCrossAxisCount
相同
class ExtentScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView(
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100,
childAspectRatio: 1,
mainAxisSpacing: 10,
crossAxisSpacing: 10
),
children: <Widget>[
Container(color: Colors.orange),
Container(color: Colors.blue),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink)
],
);
}
}
複製程式碼
GridView.extent
同樣GridView.extent
建構函式內部使用了SliverGridDelegateWithMaxCrossAxisExtent
,我們通過它可以快速的建立側軸子元素為固定最大長度的的GridView
GridView.extent({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required double maxCrossAxisExtent,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
List<Widget> children = const <Widget>[],
int semanticChildCount,
})
複製程式碼
上面SliverGridDelegateWithMaxCrossAxisExtent
中給出的示例程式碼等價於:
class ExtentScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.extent(
padding: EdgeInsets.all(10),
maxCrossAxisExtent: 100,
childAspectRatio: 1,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
children: <Widget>[
Container(color: Colors.orange),
Container(color: Colors.blue),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink)
],
);
}
}
複製程式碼
GridView.builder
- 上面我們介紹的
GridView
都需要一個Widget
陣列作為其子元素,這些方式都會提前將所有子widget
都構建好,所以只適用於子Widget
數量比較少時 - 當子
widget
比較多時,我們可以通過GridView.builder
來動態建立子Widget
GridView.builder({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required this.gridDelegate,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
})
複製程式碼
- 可以看出
GridView.builder
必須指定的引數有兩個,其中gridDelegate
之前已經介紹過了 - 屬性
itemBuilder
在之前ListView
中也有介紹過類似的, 用於構建子Widget
- 使用示例如下
class BuilderGridView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.builder(
itemCount: 50,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 10,
crossAxisSpacing: 10
),
itemBuilder: (content, index) {
return Container(
color: Colors.orange,
child: Center(
child: Text('$index'),
),
);
},
);
}
}
複製程式碼
GridView.custom
和ListView.custom
一樣, 用於構建自定義子Widget
, 有兩個必須指定的引數, 這裡就不在贅述了
const GridView.custom({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required this.gridDelegate,
@required this.childrenDelegate,
double cacheExtent,
int semanticChildCount,
})
複製程式碼
CustomScrollView
CustomScrollView
使用sliver
來自定義滾動模型(效果, 它可以包含多種滾動模型- 假設有一個頁面,頂部需要一個
GridView
,底部需要一個ListView
,而要求整個頁面的滑動效果是統一的,即它們看起來是一個整體 - 如果使用
GridView+ListView
來實現的話,就不能保證一致的滑動效果,因為它們的滾動效果是分離的,所以這時就需要一個"膠水",把這些彼此獨立的可滾動widget
"粘"起來,而CustomScrollView
的功能就相當於“膠水”
const CustomScrollView({
Key key,
// 滑動方向
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
double cacheExtent,
this.slivers = const <Widget>[],
int semanticChildCount,
})
複製程式碼
上述屬性除了slivers
之外, 前面都有提到過, 接受一個Widget
陣列, 但是這裡的Widget
必須是Sliver
型別的, 至於原因, 下面會詳解
什麼是`Sliver` ??
- 在
Flutter
中,Sliver
通常指具有特定滾動效果的可滾動塊 - 可滾動
widget
,如ListView
、GridView
等都有對應的Sliver
實現如SliverList
、SliverGrid
等 - 對於大多數
Sliver
來說,它們和可滾動Widget
最主要的區別是Sliver
不會包含Scrollable
,也就是說Sliver
本身不包含滾動互動模型 - 正因如此,
CustomScrollView
才可以將多個Sliver
"粘"在一起,這些Sliver
共用CustomScrollView
的Scrollable
,最終實現統一的滑動效果 - 前面之所以說“大多數“
Sliver
都和可滾動Widget
對應,是由於還有一些如SliverPadding
、SliverAppBar
等是和可滾動Widget
無關的 - 它們主要是為了結合
CustomScrollView
一起使用,這是因為CustomScrollView
的子widget
必須都是Sliver
SliverAppBar
AppBar
和SliverAppBar
是Material Design
中的導航欄AppBar
和SliverAppBar
都是繼承StatefulWidget
類,二者的區別在於AppBar
位置的固定的應用最上面的;而SliverAppBar
是可以跟隨內容滾動的- 其中大部分的屬性和
AppBar
都一樣
const SliverAppBar({
Key key,
// 導航欄左側weidget
this.leading,
// 如果leading為null,是否自動實現預設的leading按鈕
this.automaticallyImplyLeading = true,
// 導航欄標題
this.title,
// 導航欄右側按鈕, 接受一個陣列
this.actions,
// 一個顯示在AppBar下方的控制元件,高度和AppBar高度一樣,可以實現一些特殊的效果,該屬性通常在SliverAppBar中使用
this.flexibleSpace,
// 一個AppBarBottomWidget物件, 設定TabBar
this.bottom,
//中控制元件的z座標順序,預設值為4,對於可滾動的SliverAppBar,當 SliverAppBar和內容同級的時候,該值為0,當內容滾動 SliverAppBar 變為 Toolbar 的時候,修改elevation的值
this.elevation = 4.0,
// 背景顏色,預設值為 ThemeData.primaryColor。改值通常和下面的三個屬性一起使用
this.backgroundColor,
// 狀態列的顏色, 黑白兩種, 取值: Brightness.dark
this.brightness,
// 設定導航欄上圖示的顏色、透明度、和尺寸資訊
this.iconTheme,
// 設定導航欄上文字樣式
this.textTheme,
// 導航欄的內容是否顯示在頂部, 狀態列的下面
this.primary = true,
// 標題是否居中顯示,預設值根據不同的作業系統,顯示方式不一樣
this.centerTitle,
// 標題間距,如果希望title佔用所有可用空間,請將此值設定為0.0
this.titleSpacing = NavigationToolbar.kMiddleSpacing,
// 展開的最大高度
this.expandedHeight,
// 是否隨著華東隱藏標題
this.floating = false,
// 是否固定在頂部
this.pinned = false,
// 只跟floating相對應,如果為true,floating必須為true,也就是向下滑動一點兒,整個大背景就會動畫顯示全部,網上滑動整個導航欄的內容就會消失
this.snap = false,
})
複製程式碼
使用示例
class CustomScrollViewTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
//因為本路由沒有使用Scaffold,為了讓子級Widget(如Text)使用
//Material Design 預設的樣式風格,我們使用Material作為本路由的根。
return Material(
child: CustomScrollView(
slivers: <Widget>[
//AppBar,包含一個導航欄
SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Demo'),
background: Image.asset(
"./images/avatar.png", fit: BoxFit.cover,),
),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: new SliverGrid( //Grid
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //Grid按兩列顯示
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//建立子widget
return new Container(
alignment: Alignment.center,
color: Colors.cyan[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
),
//List
new SliverFixedExtentList(
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//建立列表項
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 50 //50個列表項
),
),
],
),
);
}
}
複製程式碼
ScrollController
ScrollController
用於控制可滾動widget
的滾動位置,這裡以ListView
為例,展示一下ScrollController
的具體用法- 最後再介紹一下路由切換時如何來儲存滾動位置
- 下面先看一下
ScrollController
的建構函式
ScrollController({
// 初始滾動位置
double initialScrollOffset = 0.0,
// 是否儲存滾動位置
this.keepScrollOffset = true,
// 除錯使用的輸出標籤
this.debugLabel,
})
複製程式碼
相關屬性和方法
offset
可滾動Widget
當前滾動的位置
jumpTo()
跳轉到指定的位置, 沒有動畫效果
void jumpTo(double value) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
for (ScrollPosition position in List<ScrollPosition>.from(_positions))
position.jumpTo(value);
}
複製程式碼
animateTo()
跳轉到指定的位置, 跳轉時會有一個動畫效果
Future<void> animateTo(double offset, {
@required Duration duration,
@required Curve curve,
}) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
final List<Future<void>> animations = List<Future<void>>(_positions.length);
for (int i = 0; i < _positions.length; i += 1)
animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
return Future.wait<void>(animations).then<void>((List<void> _) => null);
}
複製程式碼
positions
- 一個
ScrollController
可以同時被多個Scrollable
使用,ScrollController
會為每一個Scrollable
建立一個ScrollPosition
物件,這些ScrollPosition
儲存在ScrollController
的positions
屬性中(是一個陣列) ScrollPosition
是真正儲存滑動位置資訊的物件,offset
只是一個便捷屬性, 其他更多屬性可檢視相關官方文件- 一個
ScrollController
雖然可以對應多個Scrollable
,但是有一些操作,如讀取滾動位置offset
,則需要一對一,但是我們仍然可以在一對多的情況下,通過其它方法讀取滾動位置
// controller的offset屬性
double get offset => position.pixels;
// 讀取相關的滾動位置
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
複製程式碼
滾動監聽
ScrollController
間接繼承自Listenable
,我們可以根據ScrollController
來監聽滾動事件。如:
controller.addListener(()=>print(controller.offset))
複製程式碼
ScrollController控制原理
先看一下ScrollController
另外幾個方法的實現
// 建立一個儲存位置資訊的ScrollPosition
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
) {
return ScrollPositionWithSingleContext(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
// 註冊位置資訊
void attach(ScrollPosition position) {
assert(!_positions.contains(position));
_positions.add(position);
position.addListener(notifyListeners);
}
// 登出位置資訊
void detach(ScrollPosition position) {
assert(_positions.contains(position));
position.removeListener(notifyListeners);
_positions.remove(position);
}
// 銷燬ScrollController
@override
void dispose() {
for (ScrollPosition position in _positions)
position.removeListener(notifyListeners);
super.dispose();
}
複製程式碼
- 當
ScrollController
和Scrollable
關聯時,Scrollable
首先會呼叫ScrollController
的createScrollPosition()
方法來建立一個ScrollPosition
來儲存滾動位置資訊 - 然後
Scrollable
會呼叫attach()
方法,將建立的ScrollPosition
新增到ScrollController
的positions
屬性中,這一步稱為“註冊位置”,只有註冊後animateTo()
和jumpTo()
才可以被呼叫 - 當
Scrollable
銷燬時,會呼叫ScrollController
的detach()
方法,將其ScrollPosition
物件從ScrollController
的positions
屬性中移除,這一步稱為“登出位置”,登出後animateTo()
和jumpTo()
將不能再被呼叫 - 需要注意的是,
ScrollController
的animateTo()
和jumpTo()
內部會呼叫所有ScrollPosition
的animateTo()
和jumpTo()
,以實現所有和該ScrollController
關聯的Scrollable
都滾動到指定的位置
程式碼示例
建立一個ListView
,當滾動位置發生變化時,我們先列印出當前滾動位置,然後判斷當前位置是否超過1000畫素,如果超過則在螢幕右下角顯示一個“返回頂部”的按鈕,該按鈕點選後可以使ListView
恢復到初始位置;如果沒有超過1000畫素,則隱藏“返回頂部”按鈕。程式碼如下
class ScrollControllerTestRoute extends StatefulWidget {
@override
ScrollControllerTestRouteState createState() {
return new ScrollControllerTestRouteState();
}
}
class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
ScrollController _controller = new ScrollController();
bool showToTopBtn = false; //是否顯示“返回到頂部”按鈕
@override
void initState() {
//監聽滾動事件,列印滾動位置
_controller.addListener(() {
print(_controller.offset); //列印滾動位置
if (_controller.offset < 1000 && showToTopBtn) {
setState(() {
showToTopBtn = false;
});
} else if (_controller.offset >= 1000 && showToTopBtn == false) {
setState(() {
showToTopBtn = true;
});
}
});
}
@override
void dispose() {
//為了避免記憶體洩露,需要呼叫_controller.dispose
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("滾動控制")),
body: Scrollbar(
child: ListView.builder(
itemCount: 100,
itemExtent: 50.0, //列表項高度固定時,顯式指定高度是一個好習慣(效能消耗小)
controller: _controller,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
),
floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//返回到頂部時執行動畫
_controller.animateTo(.0,
duration: Duration(milliseconds: 200),
curve: Curves.ease
);
}
),
);
}
}
複製程式碼