【Flutter 專題】124 日常問題小結 (三) 自定義 Dialog 二三事

阿策小和尚發表於2021-05-28

    針對日常不同的需求,我們時常需要自定義 Dialog,而小菜在嘗試過程中遇到一些小問題,簡單記錄總結一下;

Dialog

Q1. 軟鍵盤遮擋含文字框對話方塊

    小菜在自定義含有文字框的 Dialog 時,文字框獲取焦點時,軟鍵盤會部分遮擋對話方塊,但當小菜替換為 AlertDialog 時,文字框獲取焦點時,對話方塊會向上浮動,避免軟鍵盤遮擋;

return Material(
    type: MaterialType.transparency,
    child: Stack(children: [
      Center( child: Container(
              decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20.0)),
              margin: EdgeInsets.only(left: 20.0, right: 20.0),
              child: Padding( padding: EdgeInsets.symmetric( horizontal: 20.0, vertical: 25.0),
                  child: Column(mainAxisSize: MainAxisSize.min, children: [
                    Text('賬單名稱', style: TextStyle(fontSize: 16.0)),
                    _nameWid(),
                    Row(children: [ Expanded(child: _actionButtons(0)), SizedBox(width: 15.0), Expanded(child: _actionButtons(1)) ])
                  ]))))
    ]));

Future<void> _showBillNameDialog() async {
  await showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return BillNameDialog(onCancelEvent: () {
          Navigator.pop(context);
        }, onSureEvent: (name) {
          Navigator.pop(context);
        });
      });
}
複製程式碼

A1. Scaffold & resizeToAvoidBottomInset

    對於含有文字框的自定義 Dialog,小菜在最外層使用的是 Material 巢狀,小菜通過採用 Scaffold 來巢狀處理,預設 ScaffoldresizeToAvoidBottomPadding / resizeToAvoidBottomInsettrue,當設定為 false 時,文字框獲取焦點時,依舊會被軟鍵盤遮擋;因為在固定情景可以配合 resizeToAvoidBottomPadding 實現是否被軟鍵盤遮擋效果;

    resizeToAvoidBottomPadding 主要用於自身 Widget 是否避免被其他視窗遮擋;其中小菜查資料介紹在 Flutter 1.1.9 之後更推薦使用 resizeToAvoidBottomInset

class BillNameDialog extends Dialog {
  final Function onCancelEvent;
  final Function onSureEvent;

  BillNameDialog({Key key, @required this.onCancelEvent, @required this.onSureEvent});

  @override
  Widget build(BuildContext context) {
    return Material(
        type: MaterialType.transparency,
        child: Scaffold(
            backgroundColor: Colors.transparent,
            resizeToAvoidBottomPadding: true,
            body: Stack(children: [
              Center(
                  child: Container(
                      decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.0)),
                      margin: EdgeInsets.only(left: 20.0, right: 20.0),
                      child: Padding(padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 25.0),
                          child: Column(mainAxisSize: MainAxisSize.min, children: [
                            Text('賬單名稱', style: TextStyle(fontSize: 16.0)),
                            _nameWid(),
                            Row(children: [
                              Expanded(child: _actionButtons(0)),
                              SizedBox(width: 15.0),
                              Expanded(child: _actionButtons(1))
                            ])
                          ]))))
            ])));
  }

  _nameWid() {
    return Container(
        margin: EdgeInsets.symmetric(vertical: 25.0),
        child: TextField(
            controller: TextEditingController(),
            decoration: InputDecoration(
                contentPadding: const EdgeInsets.symmetric(horizontal: 15.0),
                hintText: '建立訂單名稱',
                hintStyle: TextStyle(fontSize: 14.0),
                border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.0), borderSide: BorderSide.none),
                filled: true, fillColor: Color(0xFFf1efe5))));
  }

  _actionButtons(type) {
    return InkWell(
        child: Container(
            alignment: Alignment.center,
            decoration: BoxDecoration( color: type == 0 ? Color(0xFF1E1E1E) : Colors.deepOrange,
                border: Border.all( color: type == 0 ? Color(0xFF1E1E1E) : Colors.deepOrange), borderRadius: BorderRadius.circular(6.0)),
            child: Padding(
                padding: EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0),
                child: Text(type == 0 ? '取消' : '確認', style: TextStyle(color: Colors.white, fontSize: 15.0)))),
        onTap: () { if (type == 0) { onCancelEvent(); } else { onSureEvent(); } });
  }
}
複製程式碼

Q2. 對話方塊進行狀態更新

    小菜自定義一個可以多選 itemDialog,但 Dialog 中並沒有狀態更新的 State,如何進行 Dialog 中狀態更新呢?

class TypeListDialog extends Dialog {
  @override
  Widget build(BuildContext context) {
    return Material( type: MaterialType.transparency,
        child: Center( child: Container(
                decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.0)),
                margin: EdgeInsets.symmetric(horizontal: 25.0),
                child: Padding(padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 25.0),
                    child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
                        children: [
                          _itemTag(allType, null, 0),
                          SizedBox(height: 15.0),
                          _wrapListTag(levelType, this.data),
                          SizedBox(height: 10.0),
                          Row(mainAxisAlignment: MainAxisAlignment.end,
                              children: [ _bottomButton(0, context), SizedBox(width: 15.0), _bottomButton(1, context) ])
                        ])))));
  }

  _wrapListTag(type, tagList) {
    List<Widget> tagWid = [];
    if (tagList != null && tagList.length > 0) {
      for (int i = 0; i < tagList.length; i++) { tagWid.add(_itemTag(type, tagList, i)); }
    } else {
      tagWid.add(Container(width: 0.0, height: 0.0));
    }
    return Wrap(children: tagWid, spacing: 15.0, runSpacing: 15.0);
  }

  _bottomButton(type, context) {
    return InkWell(
        child: Container(
            decoration: BoxDecoration(color: type == 0 ? Colors.black : Colors.deepOrange,
                border: Border.all(color: type == 0 ? Colors.black : Colors.deepOrange),
                borderRadius: BorderRadius.circular(6.0)),
            child: Padding(padding: EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0),
                child: Text(type == 0 ? 'Cancel' : 'Sure', style: TextStyle(color: Colors.white, fontSize: 16.0)))),
        onTap: () { });
  }
}

Future<void> _showTypeListDialog() async {
  await showDialog(context: context, barrierDismissible: false,
      builder: (BuildContext context) { return TypeListDialog(data: levelList); });
}
複製程式碼

A2. 建立一個 StatefulBuilder 構造器

    小菜之前在 showDialog 時直接建立了 TypeListDialog,此時是無狀態的,當 WidgetBuilder 建立一個 StatefulBuilder 有狀態的構造器即可,可以將 state 傳遞到 Dialog 中;

final Function state;
TypeListDialog({Key key, this.data, @required this.state, @required this.onSelectEvent});

_itemTag(type, list, index) {
  bool _isChecked = false;
  if (allType == type) {
    _isChecked = isAllChecked;
  } else {
    _isChecked = list[index].isChecked;
  }
  return InkWell(
      key: GlobalKey(),
      child: Container(
          decoration: BoxDecoration(color: _isChecked ? Colors.deepOrange : Colors.white,
              border: Border.all(color: _isChecked ? Colors.deepOrange : Colors.black),
              borderRadius: BorderRadius.circular(18.0)),
          child: Padding(
              padding: EdgeInsets.only( top: 5.0, bottom: 5.0, left: 16.0, right: 16.0),
              child: Text(type == allType ? '全部' : list[index].name,
                  style: TextStyle(color: _isChecked ? Colors.white : Colors.black, fontSize: 16.0)))),
      onTap: () {
        if (type == levelType) {
          List.generate(data.length, (curIndex) {
            if (index == curIndex) {
              data[curIndex].isChecked = !data[curIndex].isChecked;
              if (data[curIndex].isChecked) { levelBackList.add(data[curIndex].name); }
            }
          });
          state(() {});
        }
      });
}

Future<void> _showTypeListDialog() async {
  await showDialog(context: context, barrierDismissible: false,
      builder: (BuildContext context) {
        return StatefulBuilder(builder: (context, state) => TypeListDialog(state: state, data: levelList));
      });
}
複製程式碼

Q3. Dialog 回撥傳參

    小菜在自定義 Dialog 時如何在一個回撥方法中傳遞多個引數?

A3. 接收方法與 Function 傳遞引數匹配

    小菜在 Dialog 的回撥方法中傳遞兩個 List,而在接收回撥方法中匹配兩個引數即可;小菜簡單看作是一個函式方法;

// 傳參方法
onSelectEvent(levelBackList, ['a', 'b', 'c']);

// 接收方法
Future<void> _showTypeListDialog() async {
  await showDialog(context: context,  barrierDismissible: false,
      builder: (BuildContext context) => StatefulBuilder(
          builder: (context, state) => TypeListDialog(
              state: state, data: levelList,
              onSelectEvent: (list1, list2) {
                print('list1=$list1, list2=$list2');
                Toast.show('list1=$list1, list2=$list2', context, duration: Toast.LENGTH_SHORT, gravity: Toast.BOTTOM);
              })));
}
複製程式碼

Q4. AppBar 返回按鈕

    小菜在重寫 AppBar 時,如何取消預設的返回按鈕?

A4. Material | automaticallyImplyLeading

    取消 AppBar 前面的返回圖示有多種方式;

  • Scaffold 外層巢狀 Material
@override
Widget build(BuildContext context) {
  return Material(
      child: Scaffold(
          appBar: AppBar(title: Text('Dialog Page')),
          body: _bodyWid()));
}
複製程式碼
  • AppBar 中的 automaticallyImplyLeading 屬性設定為 false
@override
Widget build(BuildContext context) {
  return Scaffold(
      appBar: AppBar(title: Text('Dialog Page'), automaticallyImplyLeading: false),
      body: _bodyWid());
}
複製程式碼
  • 在動態路由跳轉時 WidgetBuilder 設定為 MaterialPageRoute 方式;注意,此時 push 需要採用 pushReplacement / pushAndRemoveUntil 等方式;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => DialogPage()));
複製程式碼


    自定義 Dialog 案例原始碼


    小菜對於 Flutter 的應用還不夠熟悉,很多常用的場景會處理的很不到位,小菜會對日常的小問題進行簡單記錄,逐步學習;如有錯誤,請多多指導!

來源: 阿策小和尚

相關文章