Flutter 入門指北(Part 9)之彈窗和提示(SnackBar、BottomSheet、Dialog)

該文已受權公衆號 「碼個蛋」,轉載請指明出處android

前面的小節把經常使用的一些部件都介紹了,這節介紹下 Flutter 中的一些操做提示。Flutter 中的操做提示主要有這麼幾種 SnackBarBottomSheetDialog,由於 Dialog 樣式比較多,放最後講好了git

SnackBar

SnackBar 的源碼相對簡單github

const SnackBar({
    Key key,
    @required this.content, // 提示信息
    this.backgroundColor, // 背景色
    this.action, // SnackBar 尾部的按鈕,用於一些回退操做等
    this.duration = _kSnackBarDisplayDuration, // 停留的時長,默認 4000ms
    this.animation, // 進出動畫
  })
複製代碼

例如咱們須要實現一個功能,修改某個值,修改後給用戶一個提示,同時給用戶一個撤銷該操做的按鈕,那麼就能夠經過 SnackBar 來簡單實現。還有就是 SnackBar 能夠和 floatingActionButton 完美的配合,彈出的時候不會遮擋住 fabmarkdown

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 和值的變化:app

snackbar.gif

BottomSheet

BottomSheet 看命名就知道是從底部彈出的菜單,展現 BottomSheet 有兩種方式,分別是 showBottomSheetshowModalBottomSheet,兩種方式只有在展現類型上的差異,方法調用無差,並且 showBottomSheetfab 有組合動畫,showModalBottomSheet 則沒有,看下實際的例子吧。在 ListView 中增長一個 BottomSheet 的按鈕,由於 BottomSheet 須要的 context 也不能是 Scaffold 下的 context,因此須要經過 Builder 進行包裹一層,而後增長 _showBottomSheet 的方法ide

_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 就是另一種展現方式了,內部不須要作任何改變,咱們看下兩種的運行效果:post

bottom_sheet.gif

能夠看到 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,
          ),
    );
  }
複製代碼

修改高度後的效果:ui

modal_bottom_sheet.gif

Dialog

相對於 SnackBarBottomSheetDialog 的使用場景相對會更多,在 MaterialDesign 下,Dialog 主要有 3 種:AlertDialogSimpleDialogAboutDialog,固然在 Cupertino 風格下也有相應的 Dialog,由於這個系列以 MaterialDesign 風格爲主,因此 Cupertiono 等下次有時間再寫吧。this

AlertDialog

ListView 中增長一個 AlertDialog 的按鈕,用於點擊顯示 AlertDialog 用,而後加入顯示 AlertDilaog 的方法,並將按鈕的 onPressed 指向該方法,Dialogcontext 能夠是 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('你點我試試.')),
              ],
            ));
  }
複製代碼

最後看下效果:

alert_dialog.gif

SimpleDialog

SimpleDialog 相比於 AlertDialog 少了 contentaction 參數,多了 children 屬性,須要傳入 Widget 列表,那就能夠自定義所有內容了。那咱們這裏就實現一個性別選擇的 Dialog,選擇後經過 Taost 提示選擇的內容,Taost 就是以前導入的第三方插件,先看下效果圖吧

simple_dialog.gif

只要實現 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('我是個比較正經的對話框內容...你能夠隨便把我替換成任何部件,只要你喜歡(*^▽^*)')],
    );
  }
複製代碼

最後的效果:

about_dialog.gif

AboutDialog 會自帶兩個按鈕 VIEW LICENSESCLOSEVIEW LICENSES 會跳轉一個 Flutter Licenses 的網頁,CLOSE 會關閉,至於爲何是英文的,是由於咱們沒有設置語言的緣由,這個涉及到多語言,這邊推薦幾篇以前看過的文章,若是下次有時間的話會單獨拿出來說下

英文原版多語言設置,介紹兩種方式實現

國人翻譯版,未持續更新第二種方式

使用插件 in18 版

這邊爲了支持中文,咱們作下以下的修改,首先打開 pubspec.ymal 文件加入以下支持

location_01.png

get package 後給 MaterialApp 加入以下屬性,這樣就會支持中文了,這裏須要導入包 package:flutter_localizations/flutter_localizations.dart,再次運行,就會發現以前的英文變成中文了,固然你也能夠設置成別的語言。

location_02.png

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_state_01.gif

誒誒誒,怎麼 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 下的內容被一個「模態障礙」阻隔,buildercontext 和調用 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 和界面的值保持一致了

dialog_state_02.gif

以上部分代碼查看 prompt_main.dart 文件

差很少經常使用彈窗和操做提示就在這了,好好消化吧~

最後代碼的地址仍是要的:

  1. 文章中涉及的代碼:demos

  2. 基於郭神 cool weather 接口的一個項目,實現 BLoC 模式,實現狀態管理:flutter_weather

  3. 一個課程(當時買了想看下代碼規範的,代碼更新會比較慢,雖然是跟着課上的一些寫代碼,可是仍是作了本身的修改,不少地方看着不舒服,而後就改爲本身的實現方式了):flutter_shop

若是對你有幫助的話,記得給個 Star,先謝過,你的承認就是支持我繼續寫下去的動力~

相關文章
相關標籤/搜索