本節(jié)將詳細(xì)介紹一下 Flutter 中對話框的使用方式、實現(xiàn)原理、樣式定制及狀態(tài)管理。
對話框本質(zhì)上也是 UI 布局,通常一個對話框會包含標(biāo)題、內(nèi)容,以及一些操作按鈕,為此,Material 庫中提供了一些現(xiàn)成的對話框組件來用于快速的構(gòu)建出一個完整的對話框。
下面我們主要介紹一下 Material 庫中的AlertDialog
組件,它的構(gòu)造函數(shù)定義如下:
const AlertDialog({
Key key,
this.title, //對話框標(biāo)題組件
this.titlePadding, // 標(biāo)題填充
this.titleTextStyle, //標(biāo)題文本樣式
this.content, // 對話框內(nèi)容組件
this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), //內(nèi)容的填充
this.contentTextStyle,// 內(nèi)容文本樣式
this.actions, // 對話框操作按鈕組
this.backgroundColor, // 對話框背景色
this.elevation,// 對話框的陰影
this.semanticLabel, //對話框語義化標(biāo)簽(用于讀屏軟件)
this.shape, // 對話框外形
})
參數(shù)都比較簡單,不在贅述。下面我們看一個例子,假如我們要在刪除文件時彈出一個確認(rèn)對話框,該對話框如圖7-10所示:
該對話框樣式代碼如下:
AlertDialog(
title: Text("提示"),
content: Text("您確定要刪除當(dāng)前文件嗎?"),
actions: <Widget>[
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(), //關(guān)閉對話框
),
FlatButton(
child: Text("刪除"),
onPressed: () {
// ... 執(zhí)行刪除操作
Navigator.of(context).pop(true); //關(guān)閉對話框
},
),
],
);
實現(xiàn)代碼很簡單,不在贅述。唯一需要注意的是我們是通過Navigator.of(context).pop(…)
方法來關(guān)閉對話框的,這和路由返回的方式是一致的,并且都可以返回一個結(jié)果數(shù)據(jù)。現(xiàn)在,對話框我們已經(jīng)構(gòu)建好了,那么如何將它彈出來呢?還有對話框返回的數(shù)據(jù)應(yīng)如何被接收呢?這些問題的答案都在showDialog()
方法中。
showDialog()
是 Material 組件庫提供的一個用于彈出 Material 風(fēng)格對話框的方法,簽名如下:
Future<T> showDialog<T>({
@required BuildContext context,
bool barrierDismissible = true, //點擊對話框barrier(遮罩)時是否關(guān)閉它
WidgetBuilder builder, // 對話框UI的builder
})
該方法只有兩個參數(shù),含義見注釋。該方法返回一個Future
,它正是用于接收對話框的返回值:如果我們是通過點擊對話框遮罩關(guān)閉的,則Future
的值為null
,否則為我們通過Navigator.of(context).pop(result)
返回的result值,下面我們看一下整個示例:
//點擊該按鈕后彈出對話框
RaisedButton(
child: Text("對話框1"),
onPressed: () async {
//彈出對話框并等待其關(guān)閉
bool delete = await showDeleteConfirmDialog1();
if (delete == null) {
print("取消刪除");
} else {
print("已確認(rèn)刪除");
//... 刪除文件
}
},
),
// 彈出對話框
Future<bool> showDeleteConfirmDialog1() {
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Text("您確定要刪除當(dāng)前文件嗎?"),
actions: <Widget>[
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(), // 關(guān)閉對話框
),
FlatButton(
child: Text("刪除"),
onPressed: () {
//關(guān)閉對話框并返回true
Navigator.of(context).pop(true);
},
),
],
);
},
);
}
示例運行后,我們點擊對話框“取消”按鈕或遮罩,控制臺就會輸出"取消刪除",如果點擊“刪除”按鈕,控制臺就會輸出"已確認(rèn)刪除"。
注意:如果
AlertDialog
的內(nèi)容過長,內(nèi)容將會溢出,這在很多時候可能不是我們期望的,所以如果對話框內(nèi)容過長時,可以用SingleChildScrollView
將內(nèi)容包裹起來。
SimpleDialog
也是 Material 組件庫提供的對話框,它會展示一個列表,用于列表選擇的場景。下面是一個選擇 APP 語言的示例,運行結(jié)果如圖7-11。
實現(xiàn)代碼如下:
Future<void> changeLanguage() async {
int i = await showDialog<int>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('請選擇語言'),
children: <Widget>[
SimpleDialogOption(
onPressed: () {
// 返回1
Navigator.pop(context, 1);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: const Text('中文簡體'),
),
),
SimpleDialogOption(
onPressed: () {
// 返回2
Navigator.pop(context, 2);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: const Text('美國英語'),
),
),
],
);
});
if (i != null) {
print("選擇了:${i == 1 ? "中文簡體" : "美國英語"}");
}
}
列表項組件我們使用了SimpleDialogOption
組件來包裝了一下,它相當(dāng)于一個 FlatButton,只不過按鈕文案是左對齊的,并且 padding 較小。上面示例運行后,用戶選擇一種語言后,控制臺就會打印出它。
實際上AlertDialog
和SimpleDialog
都使用了Dialog
類。由于AlertDialog
和SimpleDialog
中使用了IntrinsicWidth
來嘗試通過子組件的實際尺寸來調(diào)整自身尺寸,這就導(dǎo)致他們的子組件不能是延遲加載模型的組件(如ListView
、GridView
、 CustomScrollView
等),如下面的代碼運行后會報錯。
AlertDialog(
content: ListView(
children: ...//省略
),
);
如果我們就是需要嵌套一個ListView
應(yīng)該怎么做?這時,我們可以直接使用Dialog
類,如:
Dialog(
child: ListView(
children: ...//省略
),
);
下面我們看一個彈出一個有30個列表項的對話框示例,運行效果如圖7-12所示:
實現(xiàn)代碼如下:
Future<void> showListDialog() async {
int index = await showDialog<int>(
context: context,
builder: (BuildContext context) {
var child = Column(
children: <Widget>[
ListTile(title: Text("請選擇")),
Expanded(
child: ListView.builder(
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text("$index"),
onTap: () => Navigator.of(context).pop(index),
);
},
)),
],
);
//使用AlertDialog會報錯
//return AlertDialog(content: child);
return Dialog(child: child);
},
);
if (index != null) {
print("點擊了:$index");
}
}
現(xiàn)在,我們己經(jīng)介紹完了AlertDialog
、SimpleDialog
以及Dialog
。上面的示例中,我們在調(diào)用showDialog
時,在builder
中都是構(gòu)建了這三個對話框組件的一種,可能有些讀者會慣性的以為在builder
中只能返回這三者之一,其實這不是必須的!就拿Dialog
的示例來舉例,我們完全可以用下面的代碼來替代Dialog
:
// return Dialog(child: child)
return UnconstrainedBox(
constrainedAxis: Axis.vertical,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 280),
child: Material(
child: child,
type: MaterialType.card,
),
),
);
上面代碼運行后可以實現(xiàn)一樣的效果?,F(xiàn)在我們總結(jié)一下:AlertDialog
、SimpleDialog
以及Dialog
是 Material 組件庫提供的三種對話框,旨在幫助開發(fā)者快速構(gòu)建出符合 Material 設(shè)計規(guī)范的對話框,但讀者完全可以自定義對話框樣式,因此,我們?nèi)匀豢梢詫崿F(xiàn)各種樣式的對話框,這樣即帶來了易用性,又有很強的擴展性。
我們可以把對話框分為內(nèi)部樣式和外部樣式兩部分。內(nèi)部樣式指對話框中顯示的具體內(nèi)容,這部分內(nèi)容我們已經(jīng)在上面介紹過了;外部樣式包含對話框遮罩樣式、打開動畫等,本節(jié)主要介紹如何自定義這些外部樣式。
關(guān)于動畫相關(guān)內(nèi)容我們將在本書后面章節(jié)介紹,下面內(nèi)容讀者可以先了解一下(不必深究),讀者可以在學(xué)習(xí)完動畫相關(guān)內(nèi)容后再回頭來看。
我們已經(jīng)介紹過了showDialog
方法,它是 Material 組件庫中提供的一個打開 Material 風(fēng)格對話框的方法。那如何打開一個普通風(fēng)格的對話框呢(非 Material 風(fēng)格)? Flutter 提供了一個showGeneralDialog
方法,簽名如下:
Future<T> showGeneralDialog<T>({
@required BuildContext context,
@required RoutePageBuilder pageBuilder, //構(gòu)建對話框內(nèi)部UI
bool barrierDismissible, //點擊遮罩是否關(guān)閉對話框
String barrierLabel, // 語義化標(biāo)簽(用于讀屏軟件)
Color barrierColor, // 遮罩顏色
Duration transitionDuration, // 對話框打開/關(guān)閉的動畫時長
RouteTransitionsBuilder transitionBuilder, // 對話框打開/關(guān)閉的動畫
})
實際上,showDialog
方法正是showGeneralDialog
的一個封裝,定制了 Material 風(fēng)格對話框的遮罩顏色和動畫。Material 風(fēng)格對話框打開/關(guān)閉動畫是一個Fade(漸隱漸顯)動畫,如果我們想使用一個縮放動畫就可以通過transitionBuilder
來自定義。下面我們自己封裝一個showCustomDialog
方法,它定制的對話框動畫為縮放動畫,并同時制定遮罩顏色為Colors.black87
:
Future<T> showCustomDialog<T>({
@required BuildContext context,
bool barrierDismissible = true,
WidgetBuilder builder,
}) {
final ThemeData theme = Theme.of(context, shadowThemeOnly: true);
return showGeneralDialog(
context: context,
pageBuilder: (BuildContext buildContext, Animation<double> animation,
Animation<double> secondaryAnimation) {
final Widget pageChild = Builder(builder: builder);
return SafeArea(
child: Builder(builder: (BuildContext context) {
return theme != null
? Theme(data: theme, child: pageChild)
: pageChild;
}),
);
},
barrierDismissible: barrierDismissible,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black87, // 自定義遮罩顏色
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions,
);
}
Widget _buildMaterialDialogTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
// 使用縮放動畫
return ScaleTransition(
scale: CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
),
child: child,
);
}
現(xiàn)在,我們使用showCustomDialog
打開文件刪除確認(rèn)對話框,代碼如下:
... //省略無關(guān)代碼
showCustomDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Text("您確定要刪除當(dāng)前文件嗎?"),
actions: <Widget>[
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("刪除"),
onPressed: () {
// 執(zhí)行刪除操作
Navigator.of(context).pop(true);
},
),
],
);
},
);
運行效果如圖7-13所示:
可以發(fā)現(xiàn),遮罩顏色比通過showDialog
方法打開的對話框更深。另外對話框打開/關(guān)閉的動畫已變?yōu)榭s放動畫了,讀者可以親自運行示例查看效果。
我們已經(jīng)知道對話框最終都是由showGeneralDialog
方法打開的,我們來看看它的具體實現(xiàn):
Future<T> showGeneralDialog<T>({
@required BuildContext context,
@required RoutePageBuilder pageBuilder,
bool barrierDismissible,
String barrierLabel,
Color barrierColor,
Duration transitionDuration,
RouteTransitionsBuilder transitionBuilder,
}) {
return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: barrierColor,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
));
}
實現(xiàn)很簡單,直接調(diào)用Navigator
的push
方法打開了一個新的對話框路由_DialogRoute
,然后返回了push
的返回值。可見對話框?qū)嶋H上正是通過路由的形式實現(xiàn)的,這也是為什么我們可以使用Navigator
的pop
方法來退出對話框的原因。關(guān)于對話框的樣式定制在_DialogRoute
中,沒有什么新的東西,讀者可以自行查看。
我們在用戶選擇刪除一個文件時,會詢問是否刪除此文件;在用戶選擇一個文件夾是,應(yīng)該再讓用戶確認(rèn)是否刪除子文件夾。為了在用戶選擇了文件夾時避免二次彈窗確認(rèn)是否刪除子目錄,我們在確認(rèn)對話框底部添加一個“同時刪除子目錄?”的復(fù)選框,如圖7-14所示:
現(xiàn)在就有一個問題:如何管理復(fù)選框的選中狀態(tài)?習(xí)慣上,我們會在路由頁的 State 中來管理選中狀態(tài),我們可能會寫出如下這樣的代碼:
class _DialogRouteState extends State<DialogRoute> {
bool withTree = false; // 復(fù)選框選中狀態(tài)
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
child: Text("對話框2"),
onPressed: () async {
bool delete = await showDeleteConfirmDialog2();
if (delete == null) {
print("取消刪除");
} else {
print("同時刪除子目錄: $delete");
}
},
),
],
);
}
Future<bool> showDeleteConfirmDialog2() {
withTree = false; // 默認(rèn)復(fù)選框不選中
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("您確定要刪除當(dāng)前文件嗎?"),
Row(
children: <Widget>[
Text("同時刪除子目錄?"),
Checkbox(
value: withTree,
onChanged: (bool value) {
//復(fù)選框選中狀態(tài)發(fā)生變化時重新構(gòu)建UI
setState(() {
//更新復(fù)選框狀態(tài)
withTree = !withTree;
});
},
),
],
),
],
),
actions: <Widget>[
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("刪除"),
onPressed: () {
//執(zhí)行刪除操作
Navigator.of(context).pop(withTree);
},
),
],
);
},
);
}
}
然后,當(dāng)我們運行上面的代碼時我們會發(fā)現(xiàn)復(fù)選框根本選不中!為什么會這樣呢?其實原因很簡單,我們知道setState
方法只會針對當(dāng)前 context 的子樹重新 build,但是我們的對話框并不是在_DialogRouteState
的build
方法中構(gòu)建的,而是通過showDialog
單獨構(gòu)建的,所以在_DialogRouteState
的context中調(diào)用setState
是無法影響通過showDialog
構(gòu)建的 UI 的。另外,我們可以從另外一個角度來理解這個現(xiàn)象,前面說過對話框也是通過路由的方式來實現(xiàn)的,那么上面的代碼實際上就等同于企圖在父路由中調(diào)用setState
來讓子路由更新,這顯然是不行的!簡爾言之,根本原因就是 context 不對。那如何讓復(fù)選框可點擊呢?通常有如下三種方法:
既然是 context 不對,那么直接的思路就是將復(fù)選框的選中邏輯單獨封裝成一個StatefulWidget
,然后在其內(nèi)部管理復(fù)選狀態(tài)。我們先來看看這種方法,下面是實現(xiàn)代碼:
// 單獨封裝一個內(nèi)部管理選中狀態(tài)的復(fù)選框組件
class DialogCheckbox extends StatefulWidget {
DialogCheckbox({
Key key,
this.value,
@required this.onChanged,
});
final ValueChanged<bool> onChanged;
final bool value;
@override
_DialogCheckboxState createState() => _DialogCheckboxState();
}
class _DialogCheckboxState extends State<DialogCheckbox> {
bool value;
@override
void initState() {
value = widget.value;
super.initState();
}
@override
Widget build(BuildContext context) {
return Checkbox(
value: value,
onChanged: (v) {
//將選中狀態(tài)通過事件的形式拋出
widget.onChanged(v);
setState(() {
//更新自身選中狀態(tài)
value = v;
});
},
);
}
}
下面是彈出對話框的代碼:
Future<bool> showDeleteConfirmDialog3() {
bool _withTree = false; //記錄復(fù)選框是否選中
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("您確定要刪除當(dāng)前文件嗎?"),
Row(
children: <Widget>[
Text("同時刪除子目錄?"),
DialogCheckbox(
value: _withTree, //默認(rèn)不選中
onChanged: (bool value) {
//更新選中狀態(tài)
_withTree = !_withTree;
},
),
],
),
],
),
actions: <Widget>[
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("刪除"),
onPressed: () {
// 將選中狀態(tài)返回
Navigator.of(context).pop(_withTree);
},
),
],
);
},
);
}
最后,就是使用:
RaisedButton(
child: Text("話框3(復(fù)選框可點擊)"),
onPressed: () async {
//彈出刪除確認(rèn)對話框,等待用戶確認(rèn)
bool deleteTree = await showDeleteConfirmDialog3();
if (deleteTree == null) {
print("取消刪除");
} else {
print("同時刪除子目錄: $deleteTree");
}
},
),
運行后效果如圖7-15所示:
可見復(fù)選框能選中了,點擊“取消”或“刪除”后,控制臺就會打印出最終的確認(rèn)狀態(tài)。
上面的方法雖然能解決對話框狀態(tài)更新的問題,但是有一個明顯的缺點——對話框上所有可能會改變狀態(tài)的組件都得單獨封裝在一個在內(nèi)部管理狀態(tài)的StatefulWidget
中,這樣不僅麻煩,而且復(fù)用性不大。因此,我們來想想能不能找到一種更簡單的方法?上面的方法本質(zhì)上就是將對話框的狀態(tài)置于一個StatefulWidget
的上下文中,由StatefulWidget
在內(nèi)部管理,那么我們有沒有辦法在不需要單獨抽離組件的情況下創(chuàng)建一個StatefulWidget
的上下文呢?想到這里,我們可以從Builder
組件的實現(xiàn)獲得靈感。在前面介紹過Builder
組件可以獲得組件所在位置的真正的 Context,那它是怎么實現(xiàn)的呢,我們看看它的源碼:
class Builder extends StatelessWidget {
const Builder({
Key key,
@required this.builder,
}) : assert(builder != null),
super(key: key);
final WidgetBuilder builder;
@override
Widget build(BuildContext context) => builder(context);
}
可以看到,Builder
實際上只是繼承了StatelessWidget
,然后在build
方法中獲取當(dāng)前 context 后將構(gòu)建方法代理到了builder
回調(diào),可見,Builder
實際上是獲取了StatelessWidget
的上下文(context)。那么我們能否用相同的方法獲取StatefulWidget
的上下文,并代理其build
方法呢?下面我們照貓畫虎,來封裝一個StatefulBuilder
方法:
class StatefulBuilder extends StatefulWidget {
const StatefulBuilder({
Key key,
@required this.builder,
}) : assert(builder != null),
super(key: key);
final StatefulWidgetBuilder builder;
@override
_StatefulBuilderState createState() => _StatefulBuilderState();
}
class _StatefulBuilderState extends State<StatefulBuilder> {
@override
Widget build(BuildContext context) => widget.builder(context, setState);
}
代碼很簡單,StatefulBuilder
獲取了StatefulWidget
的上下文,并代理了其構(gòu)建過程。下面我們就可以通過StatefulBuilder
來重構(gòu)上面的代碼了(變動只在DialogCheckbox
部分):
... //省略無關(guān)代碼
Row(
children: <Widget>[
Text("同時刪除子目錄?"),
//使用StatefulBuilder來構(gòu)建StatefulWidget上下文
StatefulBuilder(
builder: (context, _setState) {
return Checkbox(
value: _withTree, //默認(rèn)不選中
onChanged: (bool value) {
//_setState方法實際就是該StatefulWidget的setState方法,
//調(diào)用后builder方法會重新被調(diào)用
_setState(() {
//更新選中狀態(tài)
_withTree = !_withTree;
});
},
);
},
),
],
),
實際上,這種方法本質(zhì)上就是子組件通知父組件(StatefulWidget)重新 build 子組件本身來實現(xiàn)UI更新的,讀者可以對比代碼理解。實際上StatefulBuilder
正是 Flutter SDK 中提供的一個類,它和Builder
的原理是一樣的,在此,提醒讀者一定要將StatefulBuilder
和Builder
理解透徹,因為它們在 Flutter 中是非常實用的。
是否還有更簡單的解決方案呢?要確認(rèn)這個問題,我們就得先搞清楚UI是怎么更新的,我們知道在調(diào)用setState
方法后StatefulWidget
就會重新 build,那setState
方法做了什么呢?我們能不能從中找到方法?順著這個思路,我們就得看一下setState
的核心源碼:
void setState(VoidCallback fn) {
... //省略無關(guān)代碼
_element.markNeedsBuild();
}
可以發(fā)現(xiàn),setState
中調(diào)用了Element
的markNeedsBuild()
方法,我們前面說過,F(xiàn)lutter 是一個響應(yīng)式框架,要更新 UI 只需改變狀態(tài)后通知框架頁面需要重構(gòu)即可,而Element
的markNeedsBuild()
方法正是來實現(xiàn)這個功能的!markNeedsBuild()
方法會將當(dāng)前的Element
對象標(biāo)記為“dirty”(臟的),在每一個 Frame,F(xiàn)lutter 都會重新構(gòu)建被標(biāo)記為“dirty”Element
對象。既然如此,我們有沒有辦法獲取到對話框內(nèi)部UI的Element
對象,然后將其標(biāo)示為為“dirty”呢?答案是肯定的!我們可以通過 Context 來得到Element
對象,至于Element
與Context
的關(guān)系我們將會在后面“Flutter 核心原理”一章中再深入介紹,現(xiàn)在只需要簡單的認(rèn)為:在組件樹中,context
實際上就是Element
對象的引用。知道這個后,那么解決的方案就呼之欲出了,我們可以通過如下方式來讓復(fù)選框可以更新:
Future<bool> showDeleteConfirmDialog4() {
bool _withTree = false;
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("您確定要刪除當(dāng)前文件嗎?"),
Row(
children: <Widget>[
Text("同時刪除子目錄?"),
Checkbox( // 依然使用Checkbox組件
value: _withTree,
onChanged: (bool value) {
// 此時context為對話框UI的根Element,我們
// 直接將對話框UI對應(yīng)的Element標(biāo)記為dirty
(context as Element).markNeedsBuild();
_withTree = !_withTree;
},
),
],
),
],
),
actions: <Widget>[
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("刪除"),
onPressed: () {
// 執(zhí)行刪除操作
Navigator.of(context).pop(_withTree);
},
),
],
);
},
);
}
上面的代碼運行后復(fù)選框也可以正常選中??梢钥吹剑覀冎挥昧艘恍写a便解決了這個問題!當(dāng)然上面的代碼并不是最優(yōu),因為我們只需要更新復(fù)選框的狀態(tài),而此時的context
我們用的是對話框的根context
,所以會導(dǎo)致整個對話框 UI 組件全部 rebuild,因此最好的做法是將context
的“范圍”縮小,也就是說只將Checkbox
的 Element 標(biāo)記為dirty
,優(yōu)化后的代碼為:
... //省略無關(guān)代碼
Row(
children: <Widget>[
Text("同時刪除子目錄?"),
// 通過Builder來獲得構(gòu)建Checkbox的`context`,
// 這是一種常用的縮小`context`范圍的方式
Builder(
builder: (BuildContext context) {
return Checkbox(
value: _withTree,
onChanged: (bool value) {
(context as Element).markNeedsBuild();
_withTree = !_withTree;
},
);
},
),
],
),
showModalBottomSheet
方法可以彈出一個 Material 風(fēng)格的底部菜單列表模態(tài)對話框,示例如下:
// 彈出底部菜單列表模態(tài)對話框
Future<int> _showModalBottomSheet() {
return showModalBottomSheet<int>(
context: context,
builder: (BuildContext context) {
return ListView.builder(
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text("$index"),
onTap: () => Navigator.of(context).pop(index),
);
},
);
},
);
}
點擊按鈕,彈出該對話框:
RaisedButton(
child: Text("顯示底部菜單列表"),
onPressed: () async {
int type = await _showModalBottomSheet();
print(type);
},
),
運行后效果如圖7-16所示:
showModalBottomSheet
的實現(xiàn)原理和showGeneralDialog
實現(xiàn)原理相同,都是通過路由的方式來實現(xiàn)的,讀者可以查看源碼對比。但值得一提的是還有一個showBottomSheet
方法,該方法會從設(shè)備底部向上彈出一個全屏的菜單列表,示例如下:
// 返回的是一個controller
PersistentBottomSheetController<int> _showBottomSheet() {
return showBottomSheet<int>(
context: context,
builder: (BuildContext context) {
return ListView.builder(
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text("$index"),
onTap: (){
// do something
print("$index");
Navigator.of(context).pop();
},
);
},
);
},
);
}
運行效果如圖7-17所示:
PersistentBottomSheetController
中包含了一些控制對話框的方法比如close
方法可以關(guān)閉該對話框,功能比較簡單,讀者可以自行查看源碼。唯一需要注意的是,showBottomSheet
和我們上面介紹的彈出對話框的方法原理不同:showBottomSheet
是調(diào)用widget樹頂部的Scaffold
組件的ScaffoldState
的showBottomSheet
同名方法實現(xiàn),也就是說要調(diào)用showBottomSheet
方法就必須得保證父級組件中有Scaffold
。
其實Loading框可以直接通過showDialog
+AlertDialog
來自定義:
showLoadingDialog() {
showDialog(
context: context,
barrierDismissible: false, //點擊遮罩不關(guān)閉對話框
builder: (context) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CircularProgressIndicator(),
Padding(
padding: const EdgeInsets.only(top: 26.0),
child: Text("正在加載,請稍后..."),
)
],
),
);
},
);
}
顯示效果如圖7-18所示:
如果我們嫌 Loading 框太寬,想自定義對話框?qū)挾?,這時只使用SizedBox
或ConstrainedBox
是不行的,原因是showDialog
中已經(jīng)給對話框設(shè)置了寬度限制,根據(jù)我們在第五章“尺寸限制類容器”一節(jié)中所述,我們可以使用UnconstrainedBox
先抵消showDialog
對寬度的限制,然后再使用SizedBox
指定寬度,代碼如下:
... //省略無關(guān)代碼
UnconstrainedBox(
constrainedAxis: Axis.vertical,
child: SizedBox(
width: 280,
child: AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CircularProgressIndicator(value: .8,),
Padding(
padding: const EdgeInsets.only(top: 26.0),
child: Text("正在加載,請稍后..."),
)
],
),
),
),
);
代碼運行后,效果如圖7-19所示:
我們先看一下 Material 風(fēng)格的日歷選擇器,如圖7-20所示:
實現(xiàn)代碼:
Future<DateTime> _showDatePicker1() {
var date = DateTime.now();
return showDatePicker(
context: context,
initialDate: date,
firstDate: date,
lastDate: date.add( //未來30天可選
Duration(days: 30),
),
);
}
iOS 風(fēng)格的日歷選擇器需要使用showCupertinoModalPopup
方法和CupertinoDatePicker
組件來實現(xiàn):
Future<DateTime> _showDatePicker2() {
var date = DateTime.now();
return showCupertinoModalPopup(
context: context,
builder: (ctx) {
return SizedBox(
height: 200,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
minimumDate: date,
maximumDate: date.add(
Duration(days: 30),
),
maximumYear: date.year + 1,
onDateTimeChanged: (DateTime value) {
print(value);
},
),
);
},
);
}
運行效果如圖7-21所示:
更多建議: