一,概述
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, // 進出動畫 })
- 示例demo
假如我們需要實現一個功能,修改某個值,修改后給用戶一個提示,同時給用戶一個撤銷該操作的按鈕,那么就可以通過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); }//onTap
),//container ) //InkWell
),//generate );//ListView }把 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會關閉,至於為什么是英文的,是因為我們沒有設置語言的原因,這個涉及到多語言。
- 示例代碼
-
三,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.
- 解決辦法
所以解決的方法很明確,對上面的代碼進行修改,在外層嵌套一個 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和界面的值保持一致了