Flutter 入門指北之彈窗和提示(乾貨)
(授權轉載)微信碼個蛋授權轉載
碼個蛋(codeegg)第 607 次推文
Flutter系列文章:
《Flutter 入門指北(Part 1)之 Dart》
《Flutter 入門指北(Part 2)之基礎部件》
《Flutter 入門指北(Part 3)之 Appbar,Scaffold 填坑》
《Flutter 入門指北(Part 4)之容器部件》
《Flutter 入門指北(Part 5)之輸入處理及實戰》
《Flutter 入門指北(Part 6) 之路由》
《Flutter 入門指北(Part 7)之滑動部件》
《Flutter入門指北(Part 8)之Sliver 元件及NestedScrollView》
前面的小節把常用的一些部件都介紹了,這節介紹下 Flutter 中的一些操作提示。Flutter 中的操作提示主要有這麼幾種 SnackBar、BottomSheet、Dialog,因為 Dialog 樣式比較多,放最後講好了
SnackBar
SnackBar的原始碼相對簡單
const SnackBar({
Key key,
@required this.content, // 提示資訊
this.backgroundColor, // 背景色
this.action, // SnackBar 尾部的按鈕,用於一些回退操作等
this.duration = _kSnackBarDisplayDuration, // 停留的時長,預設 4000ms
this.animation, // 進出動畫
})
例如我們需要實現一個功能,修改某個值,修改後給使用者一個提示,同時給使用者一個撤銷該操作的按鈕,那麼就可以通過 SnackBar 來簡單實現。還有就是 SnackBar 可以和 floatingActionButton 完美的配合,彈出的時候不會遮擋住 fab
class _PromptDemoPageState extends State<PromptDemoPage> {
var count = 0;
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
// 自增操作
increase() {
setState(() => count++);
}
// 自減操作
decrease() {
setState(() => count--);
}
_changeValue(BuildContext context) {
increase();
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('當前值已修改'),
action: SnackBarAction(label: '撤銷', onPressed: decrease),
duration: Duration(milliseconds: 2000)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Prompt Demo'),
),
body: Column(children: <Widget>[
Text('當前值:$count', style: TextStyle(fontSize: 20.0)),
Expanded(
// 為了方便拓展,我這邊提取了 `snackBar` 的方法,並把按鈕放在列表
child: ListView(padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), children: <Widget>[
// SnackBar 需要提供一個包含 context,但是 context 不能是 Scaffold 節點下的 context,所以需要通過 Builder 包裹一層
Builder(builder: (context) => RaisedButton(onPressed: () => _changeValue(context), child: Text('修改當前值'))),
]))
]),
// 當 SnackBar 彈出時,fab 會上移一段距離
floatingActionButton: Builder(
builder: (context) => FloatingActionButton(onPressed: () => _changeValue(context), child: Icon(Icons.send))),
);
}
}
可以看下最後的效果圖,請注意看 fab 和值的變化:
BottomSheet
BottomSheet 看命名就知道是從底部彈出的選單,展示 BottomSheet 有兩種方式,分別是 showBottomSheet 和 showModalBottomSheet,兩種方式只有在展示型別上的差別,方法呼叫無差,而且 showBottomSheet 和 fab 有組合動畫,showModalBottomSheet 則沒有,看下實際的例子吧。在 ListView 中增加一個 BottomSheet 的按鈕,因為 BottomSheet 需要的 context 也不能是 Scaffold 下的 context,所以需要通過 Builder 進行包裹一層,然後增加 _showBottomSheet 的方法
_showBottomSheet(BuildContext context) {
showBottomSheet(
context: context,
builder: (context) => ListView(
// 生成一個列表選擇器
children: List.generate(
20,
(index) => InkWell(
child: Container(alignment: Alignment.center, height: 60.0, child: Text('Item ${index + 1}')),
onTap: () {
print('tapped item ${index + 1}');
Navigator.pop(context);
}),
)),
);
}
把 showBottomSheet 替換成 showModalBottomSheet 就是另外一種展示方式了,內部不需要做任何改變,我們看下兩種的執行效果:
可以看到 showBottomSheet 會充滿整個螢幕,然後 fab 會跟隨一起到 AppBar 的底部位置,而 showModalBottomSheet 展示的高度不會超過半個螢幕的高度,但是 fab 被其遮擋了。假如我們只需要展示 2-3 個 item,但是按照剛才的方式 showModalBottomSheet 的高度太高了,那我們可以在 ListView 外層包裹一層 Container,然後指定 height 即可
_showModalBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
child: ListView(
children: List.generate(
2,
(index) => InkWell(
child: Container(alignment: Alignment.center, height: 60.0, child: Text('Item ${index + 1}')),
onTap: () {
print('tapped item ${index + 1}');
Navigator.pop(context);
}),
)),
height: 120,
),
);
}
修改高度後的效果:
Dialog
相對於 SnackBar 和 BottomSheet,Dialog 的使用場景相對會更多,在 MaterialDesign 下,Dialog 主要有 3 種:AlertDialog,SimpleDialog 和 AboutDialog,當然在 Cupertino 風格下也有相應的 Dialog,因為這個系列以 MaterialDesign 風格為主,所以 Cupertiono 等下次有時間再寫吧。
AlertDialog
在 ListView 中增加一個 AlertDialog 的按鈕,用於點選顯示 AlertDialog 用,然後加入顯示 AlertDilaog 的方法,並將按鈕的 onPressed 指向該方法,Dialog 的 context 可以是 Scaffold 下的 context,所以不需要用 Builder 來包裹一層。
_showAlertDialog() {
showDialog(
// 設定點選 dialog 外部不取消 dialog,預設能夠取消
barrierDismissible: false,
context: context,
builder: (context) => AlertDialog(
title: Text('我是個標題...嗯,標題..'),
titleTextStyle: TextStyle(color: Colors.purple), // 標題文字樣式
content: Text(r'我是內容\(^o^)/~, 我是內容\(^o^)/~, 我是內容\(^o^)/~'),
contentTextStyle: TextStyle(color: Colors.green), // 內容文字樣式
backgroundColor: CupertinoColors.white,
elevation: 8.0, // 投影的陰影高度
semanticLabel: 'Label', // 這個用於無障礙下彈出 dialog 的提示
shape: Border.all(),
// dialog 的操作按鈕,actions 的個數儘量控制不要過多,否則會溢位 `Overflow`
actions: <Widget>[
// 點選增加顯示的值
FlatButton(onPressed: increase, child: Text('點我增加')),
// 點選減少顯示的值
FlatButton(onPressed: decrease, child: Text('點我減少')),
// 點選關閉 dialog,需要通過 Navigator 進行操作
FlatButton(onPressed: () => Navigator.pop(context),
child: Text('你點我試試.')),
],
));
}
最後看下效果:
SimpleDialog
SimpleDialog 相比於 AlertDialog 少了 content 和 action 引數,多了 children 屬性,需要傳入 Widget 列表,那就可以自定義全部內容了。那我們這裡就實現一個性別選擇的 Dialog,選擇後通過 Taost 提示選擇的內容,Taost 就是之前匯入的第三方外掛,先看下效果圖吧
只要實現children是個列表選擇器就可以了,比較簡單,直接上程式碼
_showSimpleDialog() {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => SimpleDialog(
title: Text('我是個比較正經的標題...\n選擇你的性別'),
// 這裡傳入一個選擇器列表即可
children: _genders
.map((gender) => InkWell(
child: Container(height: 40.0, child: Text(gender), alignment: Alignment.center),
onTap: () {
Navigator.pop(context);
Fluttertoast.showToast(msg: '你選擇的性別是 $gender');
},
))
.toList(),
));
}
AboutDialog
AboutDialog 主要是用於展示你的 App 或者別的相關東西的內容資訊的,平時用的比較少,顯示 AboutDialog 有兩種方式可以展示,一種是前面一樣的 showDialog 方法,傳入一個 AboutDialog 例項,還有中方法是直接呼叫 showAboutDialog 方法。我們還是一樣在列表加個按鈕,並指向顯示 AboutDialog 的事件。
_showAboutDialog() {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => AboutDialog(
// App 的名字
applicationName: 'Flutter 入門指北',
// App 的版本號
applicationVersion: '0.1.1',
// App 基本資訊下面會顯示一行小字,主要用來顯示版權資訊
applicationLegalese: 'Copyright: this is a copyright notice topically',
// App 的圖示
applicationIcon: Icon(Icons.android, size: 28.0, color: CupertinoColors.activeBlue),
// 任何你想展示的
children: <Widget>[Text('我是個比較正經的對話方塊內容...你可以隨便把我替換成任何部件,只要你喜歡(*^▽^*)')],
));
}
也可以通過showAboutDialog實現同樣的效果
_showAboutDialog() {
showAboutDialog(
context: context,
applicationName: 'Flutter 入門指北',
applicationVersion: '0.1.1',
applicationLegalese: 'Copyright: this is a copyright notice topically',
applicationIcon: Image.asset('images/app_icon.png', width: 40.0, height: 40.0),
children: <Widget>[Text('我是個比較正經的對話方塊內容...你可以隨便把我替換成任何部件,只要你喜歡(*^▽^*)')],
);
}
最後的效果:
AboutDialog 會自帶兩個按鈕 VIEW LICENSES 和 CLOSE,VIEW LICENSES 會跳轉一個 Flutter Licenses 的網頁,CLOSE 會關閉,至於為什麼是英文的,是因為我們沒有設定語言的原因,這個涉及到多語言,這邊推薦幾篇之前看過的文章,如果下次有時間的話會單獨拿出來講下
這邊為了支援中文,我們做下如下的修改,首先開啟pubspec.ymal檔案加入如下支援
get package 後給 MaterialApp 加入如下屬性,這樣就會支援中文了,這裡需要匯入包 package:flutter_localizations/flutter_localizations.dart,再次執行,就會發現之前的英文變成中文了,當然你也可以設定成別的語言。
Dialog 狀態保持
假如有個需求,需要在彈出的 Dialog 顯示當前被改變的值,然後通過按鈕可以修改這個值 ,該如何實現。相信很多小夥伴都會這麼認為,通過 setState 來修改不就行了嗎,沒錯,我一開始的確這麼去實現的,我們先看下程式碼好了,增加一個 DialogState 按鈕,然後指向對應的點選事件
_showStateDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => SimpleDialog(
title: Text('我這邊能實時修改狀態值'),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
children: <Widget>[
Text('當前的值是: $_count', style: TextStyle(fontSize: 18.0)),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[
RaisedButton(
onPressed: increase,
child: Text('點我自增'),
),
RaisedButton(
onPressed: decrease,
child: Text('點我自減'),
),
RaisedButton(
onPressed: () => Navigator.pop(context),
child: Text('點我關閉'),
)
]),
)
],
));
}
然後我們執行看下
誒誒誒,怎麼 Dialog 的值不改變呢,明明介面上的已經修改了啊。所以說圖樣圖森破咯,看下官方對 showDialog 方法的解釋吧
/// This function takes a `builder` which typically builds a [Dialog] widget.
/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
/// returned by the `builder` does not share a context with the location that
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
/// custom [StatefulWidget] if the dialog needs to update dynamically.
糟糕透的翻譯又來了:該方法通過 builder 引數來傳入一個 Dialog 部件,dialog 下的內容被一個「模態障礙」阻隔,builder 的 context 和呼叫 showDialog 時候的 context 不是共享的,如果需要動態修改 dialog 的狀態值,需要通過 StatefulBuilder 或者自定義 dialog 繼承於 StatefulWidget 來實現
所以解決的方法很明確,對上面的程式碼進行修改,在外層巢狀一個StatefulBuilder部件
_showStateDialog() {
showDialog(
context: context,
barrierDismissible: false,
// 通過 StatefulBuilder 來儲存 dialog 狀態
// builder 需要傳入一個 BuildContext 和 StateSetter 型別引數
// StateSetter 有一個 VoidCallback,修改狀態的方法在這寫
builder: (context) => StatefulBuilder(
builder: (context, dialogStateState) => SimpleDialog(
title: Text('我這邊能實時修改狀態值'),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
children: <Widget>[
Text('當前的值是:$_count', style: TextStyle(fontSize: 18.0)),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[
RaisedButton(
// 通過 StatefulBuilder 的 StateSetter 來修改值
onPressed: () => dialogStateState(() => increase()),
child: Text('點我自增'),
),
RaisedButton(
onPressed: () => dialogStateState(() => decrease()),
child: Text('點我自減'),
),
RaisedButton(
onPressed: () => Navigator.pop(context),
child: Text('點我關閉'),
)
]),
)
],
)));
}
然後再執行下,可以看到dialog和介面的值保持一致了
以上部分程式碼檢視 prompt_main.dart 檔案
差不多常用彈窗和操作提示就在這了,好好消化吧~
程式碼地址:
https://github.com/kukyxs/flutter_arts_demos_app