Flutter 之 Dialog、Alert (七十二)

对话框本质上也是UI布局,通常一个对话框会包含标题、内容,以及一些操作按钮,为此,Material库中提供了一些现成的对话框组件来用于快速的构建出一个完整的对话框。

SimpleDialog、AlertDialog、CupertinoAlertDialog、Dialog 都是最常见的弹框提示。
CupertinoAlertDialog 是 iOS 风格弹框。
showDialog、showCupertinoDialog 是两个调用弹框的 api,基本没啥区别,使用也没有什么限制。

1. Dialog 介绍

1.1 SimpleDialog

SimpleDialog是Material组件库提供的对话框,它会展示一个列表,用于列表选择的场景

SimpleDialog 定义

  const SimpleDialog({
    Key? key,
    this.title,
    this.titlePadding = const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0),
    this.titleTextStyle,
    this.children,
    this.contentPadding = const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0),
    this.backgroundColor,
    this.elevation,
    this.semanticLabel,
    this.insetPadding = _defaultInsetPadding,
    this.clipBehavior = Clip.none,
    this.shape,
    this.alignment,
  })

SimpleDialog属性

SimpleDialog属性 介绍
title 标题
titlePadding 标题内间距,默认为 const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0),
titleTextStyle 标题样式 TextStyle
children 子控件,可以随意自定义
contentPadding 内容外间距,默认为 const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0)
backgroundColor 背景色
elevation 阴影高度
semanticLabel 语义标签 (用于读屏软件)
insetPadding 对话框距离屏幕边缘间距,默认为 EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0)
clipBehavior 超出部分剪切方式,Clip.none
shape 形状 ShapeBorder
alignment 对齐方式

1.2 AlertDialog

AlertDialog 定义

  const AlertDialog({
    Key? key,
    this.title,
    this.titlePadding,
    this.titleTextStyle,
    this.content,
    this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0),
    this.contentTextStyle,
    this.actions,
    this.actionsPadding = EdgeInsets.zero,
    this.actionsAlignment,
    this.actionsOverflowDirection,
    this.actionsOverflowButtonSpacing,
    this.buttonPadding,
    this.backgroundColor,
    this.elevation,
    this.semanticLabel,
    this.insetPadding = _defaultInsetPadding,
    this.clipBehavior = Clip.none,
    this.shape,
    this.alignment,
    this.scrollable = false,
  })

AlertDialog 属性

AlertDialog属性 介绍
title 标题
titlePadding 标题内间距,默认为 const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0),
titleTextStyle 标题样式 TextStyle
content 内容控件
contentPadding 内容内间距,默认为 const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0)
contentTextStyle 内容文本样式 TextStyle
actions 事件子控件组
actionsPadding 事件子控件间距,默认为 EdgeInsets.zero,
actionsAlignment 事件子空间对齐方式 横向
actionsOverflowDirection 事件过多时,竖向展示顺序,只有正向和反向,默认为 VerticalDirection.down
actionsOverflowButtonSpacing 事件过多时,竖向展示时,子控件间距
buttonPadding actions 中每个按钮边缘填充距离,默认为左右各 8.0
backgroundColor 背景色
elevation 阴影高度
semanticLabel 语义标签 (用于读屏软件)
insetPadding 对话框距离屏幕边缘间距,默认为 EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0)
clipBehavior 超出部分剪切方式,Clip.none
shape 形状 ShapeBorder
alignment 对齐方式
scrollable 是否可以滚动,默认为 false

1.3 Dialog

实际上AlertDialog和SimpleDialog都使用了Dialog类。由于AlertDialog和SimpleDialog中使用了IntrinsicWidth来尝试通过子组件的实际尺寸来调整自身尺寸,这就导致他们的子组件不能是延迟加载模型的组件(如ListView、GridView 、 CustomScrollView等)

Dialog 定义

  const Dialog({
    Key? key,
    this.backgroundColor,
    this.elevation,
    this.insetAnimationDuration = const Duration(milliseconds: 100),
    this.insetAnimationCurve = Curves.decelerate,
    this.insetPadding = _defaultInsetPadding,
    this.clipBehavior = Clip.none,
    this.shape,
    this.alignment,
    this.child,
  })

Dialog属性

Dialog属性 介绍
backgroundColor 背景色
elevation 阴影高度
insetAnimationDuration 动画时间,默认为 const Duration(milliseconds: 100)
insetAnimationCurve 动画效果,渐进渐出等等,默认为 Curves.decelerate
insetPadding 对话框距离屏幕边缘间距,默认为 EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0)
clipBehavior 超出部分剪切方式,Clip.none
shape 形状 ShapeBorder
child 自定义弹框
alignment 对齐方式

1.4 CupertinoAlertDialog (iOS 风格)

CupertinoAlertDialog 定义

  const CupertinoAlertDialog({
    Key? key,
    this.title,
    this.content,
    this.actions = const <Widget>[],
    this.scrollController,
    this.actionScrollController,
    this.insetAnimationDuration = const Duration(milliseconds: 100),
    this.insetAnimationCurve = Curves.decelerate,
  })

CupertinoAlertDialog属性

CupertinoAlertDialog属性 介绍
title 标题
content 内容控件
actions 事件子控件组
scrollController 滚动控制器,内容超出高度,content 可以滑动
actionScrollController actions 滚动控制器,actions超出高度,actions 可以滑动
insetAnimationDuration 动画时间,默认为 const Duration(milliseconds: 100)
insetAnimationCurve 动画效果,渐进渐出等等,默认为 Curves.decelerate

2. Dialog调用方法

2.1 showDialog

showDialog 定义

Future<T?> showDialog<T>({
  required BuildContext context,
  required WidgetBuilder builder,
  bool barrierDismissible = true,
  Color? barrierColor = Colors.black54,
  String? barrierLabel,
  bool useSafeArea = true,
  bool useRootNavigator = true,
  RouteSettings? routeSettings,
})

showDialog属性

showDialog属性 介绍
context 上下文
builder (context){ return widget;} 返回一个 Widget 作为弹框展示内容
barrierDismissible 点击背后蒙层是否关闭弹框,默认为 true
barrierColor 背后蒙层颜色
barrierLabel 背后蒙版标签
useSafeArea 是否使用安全区域,默认为 true
useRootNavigator 是否使用根导航,默认为 true
routeSettings 路由设置

2.2 showCupertinoDialog

showCupertinoDialog 也是控制 Dialog 弹出的 api。其实与 showDialog 一样,两者都可以调用各种弹框,但是 showCupertinoDialog 默认是不可以点击空白区域隐藏的。

showCupertinoDialog 定义

Future<T?> showCupertinoDialog<T>({
  required BuildContext context,
  required WidgetBuilder builder,
  String? barrierLabel,
  bool useRootNavigator = true,
  bool barrierDismissible = false,
  RouteSettings? routeSettings,
})
showCupertinoDialog属性 介绍
context 上下文
builder (context){ return widget;} 返回一个 Widget 作为弹框展示内容
barrierLabel 背后蒙版标签
useRootNavigator 是否使用根导航,默认为 true
barrierDismissible 点击背后蒙层是否关闭弹框,默认为 false
routeSettings 路由设置

3. Dialog 实例

3.1 SimpleDialog 实例

示例1


class MSSimpleDialogDemo1 extends StatelessWidget {
  const MSSimpleDialogDemo1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("SimpleDialogDemo1")),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: ElevatedButton(
          onPressed: () {
            changeLanguage(context);
          },
          child: Text("语言切换"),
        ),
      ),
    );
  }

  changeLanguage(BuildContext context) {
    Future<int?> result = showDialog<int?>(
      useSafeArea: true, // 是否使用安全区域
      useRootNavigator: true, // 是否使用根Navigator
      barrierColor: Colors.black54, // 背后蒙版颜色
      barrierDismissible:true, // 点击蒙版 对话框是否消失
      context: context,
      builder: (ctx) {
        return SimpleDialog(
          title: Text("请选择语言"), // 标题
          titlePadding: EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), // 标题内间距
          titleTextStyle: TextStyle(
            fontWeight: FontWeight.w400,
            color: Colors.blue,
            fontSize: 20,
          ), // 标题样式 TextStyle
          contentPadding:
              EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), // 内容内边距 影响children布局
          alignment: Alignment.center, // 对齐方式 对话框在屏幕上的对齐方式 默认center
          backgroundColor: Colors.cyan[200], // 背景色
          insetPadding: EdgeInsets.symmetric(
              horizontal: 40.0,
              vertical:
                  24.0), // 对话框距离屏幕边缘间距,默认为 horizontal: 40.0, vertical: 24.0
          elevation: 10, // 阴影大小
          clipBehavior: Clip.none, // 超出部分剪切方式,Clip.none
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(20)), // 形状
          children: [
            SimpleDialogOption(
              padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
              child: Text("简体中文"),
              onPressed: () {
                // 关闭弹框 并且返回参数值 1
                Navigator.of(context).pop(1);
              },
            ),
            SimpleDialogOption(
              padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
              child: Text("美式英语"),
              onPressed: () {
                // 关闭弹框 并且返回参数值 2
                Navigator.of(context).pop(2);
              },
            ),
            SimpleDialogOption(
              padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
              child: Text("英式英语"),
              onPressed: () {
                // 关闭弹框 并且返回参数值 3
                Navigator.of(context).pop(3);
              },
            ),
          ],
        );
      },
    );

    // 处理选择结果
    result.then((value) {
      if (value == 1) {
        print("切换语言:简体中文");
      } else if (value == 2) {
        print("切换语言:美式英语");
      } else {
        print("切换语言:英式英语");
      }
    });
  }
}

image.png

列表项组件我们使用了SimpleDialogOption组件来包装了一下,它相当于一个TextButton,只不过按钮文案是左对齐的,并且padding较小。上面示例运行后,用户选择一种语言后,控制台就会打印出它。

示例2


class MSSimpleDialogDemo2 extends StatelessWidget {
  const MSSimpleDialogDemo2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("SimpleDialogDemo")),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: ElevatedButton(
          onPressed: () {
            _showNormalSimpleDialog(context);
          },
          child: Text("Normal SimpleDialog"),
        ),
      ),
    );
  }

  _showNormalSimpleDialog(BuildContext context) async {
    int result = await showDialog(
      context: context,
      builder: (ctx) {
        return SimpleDialog(
          title: Center(
            child: Text("SimpleDialog-Normal"),
          ),
          titleTextStyle: TextStyle(fontSize: 20, color: Colors.blue),
          titlePadding: EdgeInsets.fromLTRB(0, 20, 0, 0),
          contentPadding: EdgeInsets.all(10), // 内容外间距
          // 子控件,可以随意自定义
          children: [
            Container(
              child: Text("这就是最简单的 Dialog 了, 也可以在这里自定义样式。"),
              alignment: Alignment.center,
              padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop(1);
              },
              child: Text("知道了"),
            ),
          ],
        );
      },
    );
    print("SimpleDialog Clicked $result");
  }
}

image.png

3.2 AlertDialog 实例

示例1


class MSAlertDialogDemo1 extends StatelessWidget {
  const MSAlertDialogDemo1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("AlertDialogDemo1")),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: ElevatedButton(
          onPressed: () {
            _showDeteleAlertDialog(context);
          },
          child: Text("AlertDialogDemo1"),
        ),
      ),
    );
  }

  _showDeteleAlertDialog(BuildContext context) async {
    bool result = await showDialog(
      context: context,
      builder: (ctx) {
        return AlertDialog(
          title: Text("警告"), // 标题
          titlePadding: EdgeInsets.all(16), // 标题内边距
          titleTextStyle: TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.w400,
              color: Colors.red), // 标题样式
          content: Text("是否要删除此文件,文件一旦删除就无法找回"), // 内容组件
          contentPadding: EdgeInsets.fromLTRB(16, 0, 16, 0), // 内容内边距
          contentTextStyle:
              TextStyle(fontSize: 14, color: Colors.black), // 内容样式
          actionsAlignment: MainAxisAlignment.end, // 事件组件 对齐方式
          actionsPadding: EdgeInsets.symmetric(horizontal: 8.0), // 事件组件 内边距
          actionsOverflowButtonSpacing: 10, //事件过多时,竖向展示时,子控件间距
          actionsOverflowDirection:
              VerticalDirection.down, // 事件过多时,竖向展示顺序,只有正向和反向
          buttonPadding: EdgeInsets.symmetric(
              horizontal: 8.0), // actions 中每个按钮边缘填充距离,默认为左右各 8.0
          backgroundColor: Colors.white, // 背景色
          elevation: 10, // 阴影高度
          insetPadding: EdgeInsets.symmetric(
              horizontal: 40.0, vertical: 24.0), // 对话框距离屏幕边缘间距
          clipBehavior: Clip.none, // 超出部分剪切方式,Clip.none
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(8)), // 形状 ShapeBorder
          alignment: Alignment.center, // 对话框在屏幕上的对齐方式
          scrollable: false, // 是否可滚动
          // 事件组件 List
          actions: [
            TextButton(
              child: Text("取消", style: TextStyle(fontSize: 15)),
              onPressed: () {
                Navigator.of(context).pop(false);
              },
            ),
            TextButton(
              child: Text("删除", style: TextStyle(fontSize: 15)),
              onPressed: () {
                Navigator.of(context).pop(true);
              },
            ),
          ],
        );
      },
    );
    print("AlertDialog Item Clicked $result");
  }
}

image.png

示例2


class MSAlertDialogDemo2 extends StatelessWidget {
  const MSAlertDialogDemo2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("AlertDialogDemo2")),
      body: Padding(
        padding: EdgeInsets.all(24.0),
        child: ElevatedButton(
          onPressed: () {
            _showAlertDialog2(context);
          },
          child: Text("AlertDialog2"),
        ),
      ),
    );
  }

  _showAlertDialog2(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Text("请选择操作类型"),
          actionsOverflowButtonSpacing: 10, //事件过多时,竖向展示时,子控件间距
          actionsOverflowDirection:
              VerticalDirection.down, // 事件过多时,竖向展示顺序,只有正向和反向
          actions: [
            TextButton(
              child: Text("Action 1"),
              onPressed: () {
                Navigator.of(context).pop(1);
              },
            ),
            TextButton(
              child: Text("Action 2"),
              onPressed: () {
                Navigator.of(context).pop(2);
              },
            ),
            TextButton(
              child: Text("Action 3"),
              onPressed: () {
                Navigator.of(context).pop(3);
              },
            ),
            TextButton(
              child: Text("Action 4"),
              onPressed: () {
                Navigator.of(context).pop(4);
              },
            ),
            TextButton(
              child: Text("Action 5"),
              onPressed: () {
                Navigator.of(context).pop(5);
              },
            ),
            TextButton(
              child: Text("Action 6"),
              onPressed: () {
                Navigator.of(context).pop(6);
              },
            ),
          ],
        );
      },
    );
  }
}

image.png

注意:如果AlertDialog的内容过长,内容将会溢出,这在很多时候可能不是我们期望的,所以如果对话框内容过长时,可以用SingleChildScrollView将内容包裹起来。

3.3 Dialog 实例

Dialog 是自由度最高的弹框,样式完全根据自己给 child 的值来展示

实际上AlertDialog和SimpleDialog都使用了Dialog类。由于AlertDialog和SimpleDialog中使用了IntrinsicWidth来尝试通过子组件的实际尺寸来调整自身尺寸,这就导致他们的子组件不能是延迟加载模型的组件(如ListView、GridView 、 CustomScrollView等)

示例1


class MSDialogDemo1 extends StatelessWidget {
  const MSDialogDemo1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("DialogDemo1")),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: ElevatedButton(
          onPressed: () {
            _showCustomDialog1(context);
          },
          child: Text("CustomDialog1"),
        ),
      ),
    );
  }

  _showCustomDialog1(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) {
        return Dialog(
          backgroundColor: Colors.cyan[100], // 背景颜色
          elevation: 10, // 阴影
          insetAnimationCurve:
              Curves.decelerate, // 动画效果,渐进渐出等等,默认为 Curves.decelerate
          insetAnimationDuration: Duration(
              milliseconds: 200), // 动画时间,默认为 const Duration(milliseconds: 100)
          insetPadding: EdgeInsets.symmetric(
              horizontal: 40.0,
              vertical:
                  24.0), // 对话框距离屏幕边缘间距,默认为 EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0)
          clipBehavior: Clip.none, // 超出部分剪切方式,Clip.none
          // shape: Border.all(color: Colors.cyan, width: 1.0), // 形状 ShapeBorder
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
          alignment: Alignment.center, // 在屏幕上的对齐方式
          // 自定义内容
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 16.0),
                child: Text(
                  "提示",
                  style: TextStyle(
                    fontSize: 20,
                    color: Colors.black,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              Text(
                "是否要退出登录,退出登录将清空个人信息",
                style: TextStyle(
                  fontSize: 14,
                ),
              ),
              SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  TextButton(onPressed: () {}, child: Text("取消")),
                  TextButton(onPressed: () {}, child: Text("确定")),
                ],
              ),
            ],
          ),
        );
      },
    );
  }
}

image.png

示例2


class MSDialogDemo2 extends StatelessWidget {
  const MSDialogDemo2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("DialogDemo2")),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: ElevatedButton(
          onPressed: () {
            _showCustomDialog2(context);
          },
          child: Text("CustomDialog1"),
        ),
      ),
    );
  }

  _showCustomDialog2(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) {
        return Dialog(
          backgroundColor: Colors.amber[100],
          elevation: 10,
          insetAnimationCurve: Curves.easeIn,
          insetAnimationDuration: Duration(milliseconds: 200),
          alignment: Alignment.center,
          child: IntrinsicHeight(
            child: Column(
              children: [
                Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(
                    "请选择操作类型",
                    style: TextStyle(
                      color: Colors.blue,
                      fontSize: 20,
                      fontWeight: FontWeight.w400,
                    ),
                  ),
                ),
                SizedBox(
                  height: 300,
                  child: ListView.builder(
                    // shrinkWrap: true,
                    itemBuilder: (context, index) {
                      return ListTile(
                        leading: Icon(Icons.title),
                        title: Text("Action $index"),
                      );
                    },
                    itemExtent: 50,
                    itemCount: 8,
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

image.png

3.4 CupertinoAlertDialog 实例

示例1
使用showCupertinoDialog 调用api展示


class MSCupertinoAlertDialogDemo1 extends StatelessWidget {
  const MSCupertinoAlertDialogDemo1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("DialogDemo2")),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: ElevatedButton(
          onPressed: () {
            _showCupertinoAlertDialog1(context);
          },
          child: Text("CustomDialog1"),
        ),
      ),
    );
  }

  _showCupertinoAlertDialog1(BuildContext context) async {
    bool result = await showCupertinoDialog(
      context: context,
      builder: (context) {
        return CupertinoAlertDialog(
          title: Text("提示"), // 标题
          content: Text("是否要退出登录,退出登录后将清除用户信息?"), // 内容
          insetAnimationDuration: Duration(
              milliseconds: 200), // 动画时间,默认为 const Duration(milliseconds: 100)
          insetAnimationCurve:
              Curves.bounceIn, // 动画效果,渐进渐出等等,默认为 Curves.decelerate
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(false);
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(true);
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );

    print("用户的选择:${result ? '确定' : '取消'}");
  }
}

image.png

示例2
使用showDialog 调用api展示


class MSCupertinoAlertDialogDemo2 extends StatelessWidget {
  const MSCupertinoAlertDialogDemo2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("DialogDemo2")),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: ElevatedButton(
          onPressed: () {
            _showCupertinoAlertDialog2(context);
          },
          child: Text("CustomDialog2"),
        ),
      ),
    );
  }

  _showCupertinoAlertDialog2(BuildContext context) async {
    bool? result = await showDialog(
      context: context,
      builder: (context) {
        return CupertinoAlertDialog(
          title: Text("提示"), // 标题
          content: Text("是否要退出登录,退出登录后将清除用户信息?"), // 内容
          insetAnimationDuration: Duration(
              milliseconds: 200), // 动画时间,默认为 const Duration(milliseconds: 100)
          insetAnimationCurve:
              Curves.bounceIn, // 动画效果,渐进渐出等等,默认为 Curves.decelerate
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(false);
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(true);
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );

    if (result == null) {
      print("用户的选择:'取消'");
    } else {
      print("用户的选择:${result ? '确定' : '取消'}");
    }
  }
}

image.png

注意:使用showDialog 和showCupertinoDialog 调用api展示对话框,效果基本上是一样的,唯一的区别在于使用showCupertinoDialog 点击蒙版对话框不消失,而showDialog消失。

示例3


class MSCupertinoAlertDialogDemo3 extends StatelessWidget {
  const MSCupertinoAlertDialogDemo3({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("DialogDemo3")),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: ElevatedButton(
          onPressed: () {
            _showCupertinoAlertDialog3(context);
          },
          child: Text("CustomDialog3"),
        ),
      ),
    );
  }

  _showCupertinoAlertDialog3(BuildContext context) async {
    int? result = await showCupertinoDialog(
      context: context,
      builder: (context) {
        return CupertinoAlertDialog(
          title: Text("提示"), // 标题
          content: Text("是否要退出登录,退出登录后将清除用户信息?"), // 内容
          insetAnimationDuration: Duration(
              milliseconds: 200), // 动画时间,默认为 const Duration(milliseconds: 100)
          insetAnimationCurve:
              Curves.bounceIn, // 动画效果,渐进渐出等等,默认为 Curves.decelerate
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(0);
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(1);
              },
              child: Text("确定"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(2);
              },
              child: Text("其他"),
            ),
          ],
        );
      },
    );

    print("用户的选择:$result");
  }
}

image.png

3.5 自定义对话框

现在,我们己经介绍完了AlertDialog、SimpleDialog、Dialog以及CupertinoAlertDialog。上面的示例中,我们在调用showDialog时,在builder中都是构建了这四个对话框组件的一种,可能有些读者会惯性的以为在builder中只能返回这四者之一,其实这不是必须的!

我们完全可以用下面的代码来替代Dialog:

// return Dialog(child: child) 
return UnconstrainedBox(
  constrainedAxis: Axis.vertical,
  child: ConstrainedBox(
    constraints: BoxConstraints(maxWidth: 280),
    child: Material(
      child: child,
      type: MaterialType.card,
    ),
  ),
);

示例1


class MSCustomDialogDemo1 extends StatelessWidget {
  const MSCustomDialogDemo1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("CustomDialogDemo")),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: ElevatedButton(
          onPressed: () {
            _showCustomDialog(context);
          },
          child: Text("CustomDialogDemo"),
        ),
      ),
    );
  }

  _showCustomDialog(BuildContext context) async {
    int? result = await showDialog(
      context: context,
      builder: (ctx) {
        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),
                );
              },
            )),
          ],
        );
        return UnconstrainedBox(
          constrainedAxis: Axis.vertical,
          child: ConstrainedBox(
            constraints: BoxConstraints(maxWidth: 280, maxHeight: 280),
            child: Material(
              child: child,
              type: MaterialType.card,
            ),
          ),
        );
      },
    );
  }
}


image.png

4. 对话框打开动画及遮罩

可以把对话框分为内部样式和外部样式两部分。内部样式指对话框中显示的具体内容,这部分内容我们已经在上面介绍过了;外部样式包含对话框遮罩样式、打开动画等

showDialog 是Material组件库中提供的一个打开Material风格对话框的方法,如果我们要打开一个普通风格的对话框呢(非Material风格)可以使用showGeneralDialog方法。

showGeneralDialog 定义

Future<T?> showGeneralDialog<T extends Object?>({
  required BuildContext context,
  required RoutePageBuilder pageBuilder, // 构建对话框内部UI
  bool barrierDismissible = false, //点击遮罩是否关闭对话框
  String? barrierLabel,  // 语义化标签(用于读屏软件)
  Color barrierColor = const Color(0x80000000), // 遮罩颜色
  Duration transitionDuration = const Duration(milliseconds: 200), // 对话框打开/关闭的动画时长
  RouteTransitionsBuilder? transitionBuilder, // 对话框打开/关闭的动画
  bool useRootNavigator = true,
  RouteSettings? routeSettings,
})

实际上,showDialog方法正是showGeneralDialog的一个封装,定制了Material风格对话框的遮罩颜色和动画。Material风格对话框打开/关闭动画是一个Fade(渐隐渐显)动画,如果我们想使用一个缩放动画就可以通过transitionBuilder来自定义。

下面我们自己封装一个showCustomDialog方法,它定制的对话框动画为缩放动画,并同时制定遮罩颜色为Colors.black87:


Future<T?> showCustomGeneralDialog<T>({
  required BuildContext context,
  bool barrierDismissible = true,
  required WidgetBuilder builder,
}) {
  final ThemeData theme = Theme.of(context);
  return showGeneralDialog(
    context: context,
    pageBuilder: (BuildContext context, Animation<double> animation,
        Animation<double> secondaryAnimation) {
      final Widget pageChild = Builder(builder: builder);
      return SafeArea(
        child: Builder(builder: (context) {
          return theme != null
              ? Theme(data: theme, child: pageChild)
              : pageChild;
        }),
      );
    },
    barrierDismissible: barrierDismissible,
    barrierColor: Colors.black87,
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
    transitionDuration: Duration(milliseconds: 150),
    transitionBuilder: (context, animation, secondaryAnimation, child) {
      return ScaleTransition(
        scale: CurvedAnimation(
          parent: animation,
          curve: Curves.easeIn,
        ),
        child: child,
      );
    },
  );
}

现在,我们使用showCustomDialog打开文件删除确认对话框,代码如下:


  _showCustomDialog1(BuildContext context) async {
    int? result = await showCustomGeneralDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("警告"),
          content: Text("您确定要删除当前文件吗"),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(0);
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(1);
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );
  }

完整代码


Future<T?> showCustomGeneralDialog<T>({
  required BuildContext context,
  bool barrierDismissible = true,
  required WidgetBuilder builder,
}) {
  final ThemeData theme = Theme.of(context);
  return showGeneralDialog(
    context: context,
    pageBuilder: (BuildContext context, Animation<double> animation,
        Animation<double> secondaryAnimation) {
      final Widget pageChild = Builder(builder: builder);
      return SafeArea(
        child: Builder(builder: (context) {
          return theme != null
              ? Theme(data: theme, child: pageChild)
              : pageChild;
        }),
      );
    },
    barrierDismissible: barrierDismissible,
    barrierColor: Colors.black87,
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
    transitionDuration: Duration(milliseconds: 150),
    transitionBuilder: (context, animation, secondaryAnimation, child) {
      return ScaleTransition(
        scale: CurvedAnimation(
          parent: animation,
          curve: Curves.easeIn,
        ),
        child: child,
      );
    },
  );
}


class MSCustomGeneralDialogDemo1 extends StatelessWidget {
  const MSCustomGeneralDialogDemo1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("CustomGeneralDialogDemo1")),
      body: Padding(
        padding: EdgeInsets.all(24.0),
        child: ElevatedButton(
          onPressed: () {
            _showCustomDialog1(context);
          },
          child: Text("MSCustomGeneralDialog"),
        ),
      ),
    );
  }

  _showCustomDialog1(BuildContext context) async {
    int? result = await showCustomGeneralDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("警告"),
          content: Text("您确定要删除当前文件吗"),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(0);
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(1);
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );
  }

}

116.gif

可以发现,遮罩颜色比通过showDialog方法打开的对话框更深。另外对话框打开/关闭的动画已变为缩放动画了。

5. 对话框实现原理

我们以showGeneralDialog方法为例来看看它的具体实现:

Future<T?> showGeneralDialog<T extends Object?>({
  required BuildContext context,
  required RoutePageBuilder pageBuilder,
  bool barrierDismissible = false,
  String? barrierLabel,
  Color barrierColor = const Color(0x80000000),
  Duration transitionDuration = const Duration(milliseconds: 200),
  RouteTransitionsBuilder? transitionBuilder,
  bool useRootNavigator = true,
  RouteSettings? routeSettings,
}) {
  assert(pageBuilder != null);
  assert(useRootNavigator != null);
  assert(!barrierDismissible || barrierLabel != null);
  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(RawDialogRoute<T>(
    pageBuilder: pageBuilder,
    barrierDismissible: barrierDismissible,
    barrierLabel: barrierLabel,
    barrierColor: barrierColor,
    transitionDuration: transitionDuration,
    transitionBuilder: transitionBuilder,
    settings: routeSettings,
  ));
}

实现很简单,直接调用Navigator的push方法打开了一个新的对话框路由RawDialogRoute,然后返回了push的返回值。可见对话框实际上正是通过路由的形式实现的,这也是为什么我们可以使用Navigator的pop 方法来退出对话框的原因。

6. 对话框状态管理

我们在用户选择删除一个文件时,会询问是否删除此文件;在用户选择一个文件夹时,应该再让用户确认是否删除子文件夹。为了在用户选择了文件夹时避免二次弹窗确认是否删除子目录,我们在确认对话框底部添加一个“同时删除子目录?”的复选框。

image.png

现在就有一个问题:如何管理复选框的选中状态?习惯上,我们会在路由页的State中来管理选中状态,我们可能会写出如下这样的代码:


class MSDialogStatusDemo1 extends StatefulWidget {
  const MSDialogStatusDemo1({Key? key}) : super(key: key);

  @override
  State<MSDialogStatusDemo1> createState() => _MSDialogStatusDemo1State();
}

class _MSDialogStatusDemo1State extends State<MSDialogStatusDemo1> {
  bool _delete = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("DialogStatusDemo1")),
      body: Padding(
        padding: EdgeInsets.all(24.0),
        child: ElevatedButton(
          onPressed: () async {
            bool? result = await _showDeleteDialog2(context);
            if (result == null) {
              print("取消删除");
            } else {
              print("同时删除子目录: $_delete");
            }
          },
          child: Text("对话框2"),
        ),
      ),
    );
  }

  Future<bool?> _showDeleteDialog2(BuildContext context) {
    return showDialog<bool>(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text("您确定要删除当前文件吗?"),
              Row(
                children: [
                  Text("同时删除子目录?"),
                  Checkbox(
                    value: _delete,
                    onChanged: (value) {
                      setState(() {
                        _delete = value!;
                      });
                    },
                  ),
                ],
              )
            ],
          ),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(_delete);
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );
  }
}

当我们运行上面的代码时我们会发现复选框根本选不中!为什么会这样呢?其实原因很简单,我们知道setState方法只会针对当前context的子树重新build,但是我们的对话框并不是在_MSDialogStatusDemo1Statebuild 方法中构建的,而是通过showDialog单独构建的,所以在_MSDialogStatusDemo1State的context中调用setState是无法影响通过showDialog构建的UI的。另外,我们可以从另外一个角度来理解这个现象,前面说过对话框也是通过路由的方式来实现的,那么上面的代码实际上就等同于企图在父路由中调用setState来让子路由更新,这显然是不行的!简尔言之,根本原因就是context不对。那如何让复选框可点击呢?通常有如下三种方法:

6.1 单独抽离出StatefulWidget

既然是context不对,那么直接的思路就是将复选框的选中逻辑单独封装成一个StatefulWidget,然后在其内部管理复选状态。我们先来看看这种方法,下面是实现代码:


class MSDialogCheckbox extends StatefulWidget {
  const MSDialogCheckbox(
      {Key? key, required this.onChanged, this.value = false})
      : super(key: key);

  final ValueChanged<bool?> onChanged;
  final bool? value;

  @override
  State<MSDialogCheckbox> createState() => _MSDialogCheckboxState();
}

class _MSDialogCheckboxState extends State<MSDialogCheckbox> {
  bool? value;
  @override
  void initState() {
    value = widget.value;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Checkbox(
      value: value,
      onChanged: (v) {
        setState(() {
          value = v;
          widget.onChanged(value);
        });
      },
    );
  }
}

完整代码


class MSDialogStatusDemo2 extends StatelessWidget {
  MSDialogStatusDemo2({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("DialogStatusDemo2")),
      body: Padding(
        padding: EdgeInsets.all(24.0),
        child: ElevatedButton(
          child: Text("对话框2"),
          onPressed: () async {
            //弹出删除确认对话框,等待用户确认
            bool? deleteTree = await _showDeleteDialog2(context);
            if (deleteTree == null) {
              print("取消删除");
            } else {
              print("同时删除子目录: $deleteTree");
            }
          },
        ),
      ),
    );
  }

  Future<bool?> _showDeleteDialog2(BuildContext context) {
    bool _withTree = false; //记录复选框是否选中
    return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text("您确定要删除当前文件吗?"),
              Row(
                children: [
                  Text("同时删除子目录?"),
                  MSDialogCheckbox(
                    onChanged: (v) {
                      //更新选中状态
                      _withTree = v!;
                    },
                  ),
                ],
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                // 将选中状态返回
                Navigator.of(context).pop(_withTree);
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );
  }
}

class MSDialogCheckbox extends StatefulWidget {
  const MSDialogCheckbox(
      {Key? key, required this.onChanged, this.value = false})
      : super(key: key);

  final ValueChanged<bool?> onChanged;
  final bool? value;

  @override
  State<MSDialogCheckbox> createState() => _MSDialogCheckboxState();
}

class _MSDialogCheckboxState extends State<MSDialogCheckbox> {
  bool? value;
  @override
  void initState() {
    value = widget.value;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Checkbox(
      value: value,
      onChanged: (v) {
        setState(() {
          value = v;
          widget.onChanged(value);
        });
      },
    );
  }
}

117.gif

可见复选框能选中了,点击“取消”或“删除”后,控制台就会打印出最终的确认状态。

6.2 使用StatefulBuilder方法

上面的方法虽然能解决对话框状态更新的问题,但是有一个明显的缺点——对话框上所有可能会改变状态的组件都得单独封装在一个在内部管理状态的StatefulWidget中,这样不仅麻烦,而且复用性不大。因此,我们来想想能不能找到一种更简单的方法?上面的方法本质上就是将对话框的状态置于一个StatefulWidget的上下文中,由StatefulWidget在内部管理,那么我们有没有办法在不需要单独抽离组件的情况下创建一个StatefulWidget的上下文呢?想到这里,我们可以从Builder组件的实现获得灵感。Builder组件可以获得组件所在位置的真正的Context,那它是怎么实现的呢,我们看看它的源码:

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方法中获取当前context后将构建方法代理到了builder回调,可见,Builder实际上是获取了StatelessWidget 的上下文(context)。StatefulBuilder 和Builder类似

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的上下文,并代理了其构建过程。下面我们就可以通过StatefulBuilder来重构上面的代码了(变动只在DialogCheckbox部分):

... //省略无关代码
Row(
  children: <Widget>[
    Text("同时删除子目录?"),
    //使用StatefulBuilder来构建StatefulWidget上下文
    StatefulBuilder(
      builder: (context, _setState) {
        return Checkbox(
          value: _withTree, //默认不选中
          onChanged: (bool value) {
            //_setState方法实际就是该StatefulWidget的setState方法,
            //调用后builder方法会重新被调用
            _setState(() {
              //更新选中状态
              _withTree = !_withTree;
            });
          },
        );
      },
    ),
  ],
),

完整代码


class MSDialogStatusDemo3 extends StatelessWidget {
  const MSDialogStatusDemo3({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("DialogStatusDemo3"),
      ),
      body: Padding(
        padding: EdgeInsets.all(24.0),
        child: ElevatedButton(
          child: Text("对话框3"),
          onPressed: () async {
            bool? delete = await _showDeleteDialog3(context);
            if (delete == null) {
              print("取消删除");
            } else {
              print("同时删除子目录: $delete");
            }
          },
        ),
      ),
    );
  }

  _showDeleteDialog3(BuildContext context) {
    bool _withTree = false; //记录复选框是否选中
    return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text("您确定要删除当前文件吗?"),
              Row(
                children: [
                  Text("同时删除子目录?"),
                  StatefulBuilder(
                    builder: (context, _setState) {
                      return Checkbox(
                        value: _withTree,
                        onChanged: (value) {
                          _setState(() {
                            _withTree = value!;
                          });
                        },
                      );
                    },
                  ),
                ],
              )
            ],
          ),
          actions: [
            TextButton(
              child: Text("取消"),
              onPressed: () => Navigator.of(context).pop(),
            ),
            TextButton(
              child: Text("删除"),
              onPressed: () {
                // 将选中状态返回
                Navigator.of(context).pop(_withTree);
              },
            ),
          ],
        );
      },
    );
  }
}

实际上,这种方法本质上就是子组件通知父组件(StatefulWidget)重新build子组件本身来实现UI更新的,实际上StatefulBuilder正是Flutter SDK中提供的一个类,它和Builder的原理是一样的

6.3 精妙的解法

是否还有更简单的解决方案呢?要确认这个问题,我们就得先搞清楚UI是怎么更新的,我们知道在调用setState方法后StatefulWidget就会重新build,那setState方法做了什么呢?我们能不能从中找到方法?顺着这个思路,我们就得看一下setState的核心源码:

void setState(VoidCallback fn) {
  ... //省略无关代码
  _element.markNeedsBuild();
}

可以发现,setState中调用了Element的markNeedsBuild()方法,我们前面说过,Flutter是一个响应式框架,要更新UI只需改变状态后通知框架页面需要重构即可,而Element的markNeedsBuild()方法正是来实现这个功能的!markNeedsBuild()方法会将当前的Element对象标记为“dirty”(脏的),在每一个Frame,Flutter都会重新构建被标记为“dirty”Element对象。既然如此,我们有没有办法获取到对话框内部UI的Element对象,然后将其标示为为“dirty”呢?答案是肯定的
在组件树中,context实际上就是Element对象的引用,可以通过Context来得到Element对象。执行Element的markNeedsBuild方法就可以让复选框重新刷新

核心代码

Checkbox(
  value: _withTree,
  onChanged: (value) {
    // 此时context为对话框UI的根Element,我们
    // 直接将对话框UI对应的Element标记为dirty
    (context as Element).markNeedsBuild();
    _withTree = !_withTree;
  },
),

  _showDeleteDialog4(BuildContext context) {
    bool _withTree = false; //记录复选框是否选中
    return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text("您确定要删除当前文件吗?"),
              Row(
                children: [
                  Text("同时删除子目录?"),
                  Checkbox(
                    value: _withTree,
                    onChanged: (value) {
                      // 此时context为对话框UI的根Element,我们
                      // 直接将对话框UI对应的Element标记为dirty
                      (context as Element).markNeedsBuild();
                      _withTree = !_withTree;
                    },
                  ),
                ],
              )
            ],
          ),
          actions: [
            TextButton(
              child: Text("取消"),
              onPressed: () => Navigator.of(context).pop(),
            ),
            TextButton(
              child: Text("删除"),
              onPressed: () {
                // 将选中状态返回
                Navigator.of(context).pop(_withTree);
              },
            ),
          ],
        );
      },
    );
  }

上面的代码运行后复选框也可以正常选中。可以看到,我们只用了一行代码便解决了这个问题!当然上面的代码并不是最优,因为我们只需要更新复选框的状态,而此时的context我们用的是对话框的根context,所以会导致整个对话框UI组件全部rebuild,因此最好的做法是将context的“范围”缩小,也就是说只将Checkbox的Element标记为dirty,优化后的代码为:

... //省略无关代码
Row(
  children: <Widget>[
    Text("同时删除子目录?"),
    // 通过Builder来获得构建Checkbox的`context`,
    // 这是一种常用的缩小`context`范围的方式
    Builder(
      builder: (BuildContext context) {
        return Checkbox(
          value: _withTree,
          onChanged: (bool value) {
            (context as Element).markNeedsBuild();
            _withTree = !_withTree;
          },
        );
      },
    ),
  ],
),

7. 其它类型的对话框

7.1 底部菜单列表

showModalBottomSheet方法可以弹出一个Material风格的底部菜单列表模态对话框,示例如下:

// 弹出底部菜单列表模态对话框
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),
          );
        },
      );
    },
  );
}

点击按钮,弹出该对话框:

ElevatedButton(
  child: Text("显示底部菜单列表"),
  onPressed: () async {
    int type = await _showModalBottomSheet();
    print(type);
  },
),

运行后效果如下所示:


image.png

7.2 Loading框

其实Loading框可以直接通过showDialog+AlertDialog来自定义:

showLoadingDialog() {
  showDialog(
    context: context,
    barrierDismissible: false, //点击遮罩不关闭对话框
    builder: (context) {
      return AlertDialog(
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            CircularProgressIndicator(),
            Padding(
              padding: const EdgeInsets.only(top: 26.0),
              child: Text("正在加载,请稍后..."),
            )
          ],
        ),
      );
    },
  );
}

显示效果如图7-18所示:


image.png

如果我们嫌Loading框太宽,想自定义对话框宽度,这时只使用SizedBox或ConstrainedBox是不行的,原因是showDialog中已经给对话框设置了最小宽度约束,我们可以使用UnconstrainedBox先抵消showDialog对宽度的约束,然后再使用SizedBox指定宽度,代码如下:

... //省略无关代码
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所示:

image.png

7.3 日历选择器

我们先看一下Material风格的日历选择器,如图7-20所示:

image.png

实现代码

Future<DateTime?> _showDatePicker1() {
  var date = DateTime.now();
  return showDatePicker(
    context: context,
    initialDate: date,
    firstDate: date,
    lastDate: date.add( //未来30天可选
      Duration(days: 30),
    ),
  );
}

iOS风格的日历选择器需要使用showCupertinoModalPopup方法和CupertinoDatePicker组件来实现:

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所示:

image.png

7.4 showAboutDialog


class MSAboutDialogDemo extends StatelessWidget {
  const MSAboutDialogDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("AboutDialogDemo")),
      body: Padding(
        padding: EdgeInsets.all(24.0),
        child: ElevatedButton(
          child: Text("AboutDialog"),
          onPressed: () {
            showAboutDialog(
              context: context,
              applicationName: "learn_flutter",
              applicationVersion: "1.0.0.1",
              applicationLegalese: "本App解释权归本公司所有",
              applicationIcon: FlutterLogo(
                size: 60,
              ),
            );
          },
        ),
      ),
    );
  }
}

image.png

8. Demo


class MSDialogDemo extends StatelessWidget {
  const MSDialogDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("DialogDemo"),
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Center(
            child: Column(
              children: [
                ElevatedButton(
                  onPressed: () {
                    _showSimpleDialog1(context).then((value) {
                      if (value == null) {
                        print("取消选择");
                        return;
                      }
                      if (value == 1) {
                        print("切换语言:简体中文");
                      } else if (value == 2) {
                        print("切换语言:美式英语");
                      } else {
                        print("切换语言:英式英语");
                      }
                    });
                  },
                  child: Text("SimpleDialog1"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    int? result = await _showSimpleDialog2(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("SimpleDialog2"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    bool? result = await _showAlertDialog1(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("AlertDialog1"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    int? result = await _showAlertDialog2(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("AlertDialog2"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    int? result = await _showCustomDialog1(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("Dialog1"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    int? result = await _showCustomDialog2(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("Dialog2"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    bool? result = await _showCupertinoAlertDialog1(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("CupertinoAlertDialog1"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    int? result = await _showCupertinoAlertDialog2(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("CupertinoAlertDialog2"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    int? result = await _showCustomDialog3(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("CustomDialog1"),
                ),
                ElevatedButton(
                  onPressed: () async {
                    int? result = await _showCustomDialog4(context);
                    if (result == null) {
                      print("取消选择");
                    } else {
                      print("选择 $result");
                    }
                  },
                  child: Text("CustomDialog2"),
                ),
                ElevatedButton(
                  child: Text("CheckBoxDialog1"),
                  onPressed: () async {
                    bool? delete = await _showCheckBoxDialog1(context);
                    if (delete == null) {
                      print("取消删除");
                    } else {
                      print("同时删除子目录: $delete");
                    }
                  },
                ),
                ElevatedButton(
                  child: Text("CheckBoxDialog2"),
                  onPressed: () async {
                    bool? delete = await _showCheckBoxDialog2(context);
                    if (delete == null) {
                      print("取消删除");
                    } else {
                      print("同时删除子目录: $delete");
                    }
                  },
                ),
                ElevatedButton(
                  child: Text("显示底部菜单列表"),
                  onPressed: () async {
                    int? type = await _showModalBottomSheet(context);
                    print(type);
                  },
                ),
                ElevatedButton(
                  child: Text("LoadingDialog"),
                  onPressed: () {
                    _showLoadingDialog(context);
                  },
                ),
                ElevatedButton(
                  child: Text("Material DatePicker"),
                  onPressed: () {
                    _showDatePicker1(context);
                  },
                ),
                ElevatedButton(
                  child: Text("IOS DatePicker"),
                  onPressed: () {
                    _showDatePicker2(context);
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Future<int?> _showSimpleDialog1(BuildContext context) {
    return showDialog<int?>(
      useSafeArea: true, // 是否使用安全区域
      useRootNavigator: true, // 是否使用根Navigator
      barrierColor: Colors.black54, // 背后蒙版颜色
      barrierDismissible: true, // 点击蒙版 对话框是否消失
      context: context,
      builder: (ctx) {
        return SimpleDialog(
          title: Text("请选择语言"), // 标题
          titlePadding: EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), // 标题内间距
          titleTextStyle: TextStyle(
            fontWeight: FontWeight.w400,
            color: Colors.blue,
            fontSize: 20,
          ), // 标题样式 TextStyle
          contentPadding:
              EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), // 内容内边距 影响children布局
          alignment: Alignment.center, // 对齐方式 对话框在屏幕上的对齐方式 默认center
          backgroundColor: Colors.cyan[200], // 背景色
          insetPadding: EdgeInsets.symmetric(
              horizontal: 40.0,
              vertical:
                  24.0), // 对话框距离屏幕边缘间距,默认为 horizontal: 40.0, vertical: 24.0
          elevation: 10, // 阴影大小
          clipBehavior: Clip.none, // 超出部分剪切方式,Clip.none
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(20)), // 形状
          children: [
            SimpleDialogOption(
              padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
              child: Text("简体中文"),
              onPressed: () {
                // 关闭弹框 并且返回参数值 1
                Navigator.of(context).pop(1);
              },
            ),
            SimpleDialogOption(
              padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
              child: Text("美式英语"),
              onPressed: () {
                // 关闭弹框 并且返回参数值 2
                Navigator.of(context).pop(2);
              },
            ),
            SimpleDialogOption(
              padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
              child: Text("英式英语"),
              onPressed: () {
                // 关闭弹框 并且返回参数值 3
                Navigator.of(context).pop(3);
              },
            ),
          ],
        );
      },
    );
  }

  Future<int?> _showSimpleDialog2(BuildContext context) {
    return showDialog(
      context: context,
      builder: (ctx) {
        return SimpleDialog(
          title: Center(
            child: Text("SimpleDialog-Normal"),
          ),
          titleTextStyle: TextStyle(fontSize: 20, color: Colors.blue),
          titlePadding: EdgeInsets.fromLTRB(0, 20, 0, 0),
          contentPadding: EdgeInsets.all(10), // 内容外间距
          // 子控件,可以随意自定义
          children: [
            Container(
              child: Text("这就是最简单的 Dialog 了, 也可以在这里自定义样式。"),
              alignment: Alignment.center,
              padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop(1);
              },
              child: Text("知道了"),
            ),
          ],
        );
      },
    );
  }

  Future<bool?> _showAlertDialog1(BuildContext context) {
    return showDialog(
      context: context,
      builder: (ctx) {
        return AlertDialog(
          title: Text("警告"), // 标题
          titlePadding: EdgeInsets.all(16), // 标题内边距
          titleTextStyle: TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.w400,
              color: Colors.red), // 标题样式
          content: Text("是否要删除此文件,文件一旦删除就无法找回"), // 内容组件
          contentPadding: EdgeInsets.fromLTRB(16, 0, 16, 0), // 内容内边距
          contentTextStyle:
              TextStyle(fontSize: 14, color: Colors.black), // 内容样式
          actionsAlignment: MainAxisAlignment.end, // 事件组件 对齐方式
          actionsPadding: EdgeInsets.symmetric(horizontal: 8.0), // 事件组件 内边距
          actionsOverflowButtonSpacing: 10, //事件过多时,竖向展示时,子控件间距
          actionsOverflowDirection:
              VerticalDirection.down, // 事件过多时,竖向展示顺序,只有正向和反向
          buttonPadding: EdgeInsets.symmetric(
              horizontal: 8.0), // actions 中每个按钮边缘填充距离,默认为左右各 8.0
          backgroundColor: Colors.white, // 背景色
          elevation: 10, // 阴影高度
          insetPadding: EdgeInsets.symmetric(
              horizontal: 40.0, vertical: 24.0), // 对话框距离屏幕边缘间距
          clipBehavior: Clip.none, // 超出部分剪切方式,Clip.none
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(8)), // 形状 ShapeBorder
          alignment: Alignment.center, // 对话框在屏幕上的对齐方式
          scrollable: false, // 是否可滚动
          // 事件组件 List
          actions: [
            TextButton(
              child: Text("取消", style: TextStyle(fontSize: 15)),
              onPressed: () {
                Navigator.of(context).pop(false);
              },
            ),
            TextButton(
              child: Text("删除", style: TextStyle(fontSize: 15)),
              onPressed: () {
                Navigator.of(context).pop(true);
              },
            ),
          ],
        );
      },
    );
  }

  Future<int?> _showAlertDialog2(BuildContext context) {
    return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Text("请选择操作类型"),
          actionsOverflowButtonSpacing: 10, //事件过多时,竖向展示时,子控件间距
          actionsOverflowDirection:
              VerticalDirection.down, // 事件过多时,竖向展示顺序,只有正向和反向
          actions: [
            TextButton(
              child: Text("Action 1"),
              onPressed: () {
                Navigator.of(context).pop(1);
              },
            ),
            TextButton(
              child: Text("Action 2"),
              onPressed: () {
                Navigator.of(context).pop(2);
              },
            ),
            TextButton(
              child: Text("Action 3"),
              onPressed: () {
                Navigator.of(context).pop(3);
              },
            ),
            TextButton(
              child: Text("Action 4"),
              onPressed: () {
                Navigator.of(context).pop(4);
              },
            ),
            TextButton(
              child: Text("Action 5"),
              onPressed: () {
                Navigator.of(context).pop(5);
              },
            ),
            TextButton(
              child: Text("Action 6"),
              onPressed: () {
                Navigator.of(context).pop(6);
              },
            ),
          ],
        );
      },
    );
  }

  Future<int?> _showCustomDialog1(BuildContext context) {
    return showDialog(
      context: context,
      builder: (context) {
        return Dialog(
          backgroundColor: Colors.cyan[100], // 背景颜色
          elevation: 10, // 阴影
          insetAnimationCurve:
              Curves.decelerate, // 动画效果,渐进渐出等等,默认为 Curves.decelerate
          insetAnimationDuration: Duration(
              milliseconds: 200), // 动画时间,默认为 const Duration(milliseconds: 100)
          insetPadding: EdgeInsets.symmetric(
              horizontal: 40.0,
              vertical:
                  24.0), // 对话框距离屏幕边缘间距,默认为 EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0)
          clipBehavior: Clip.none, // 超出部分剪切方式,Clip.none
          // shape: Border.all(color: Colors.cyan, width: 1.0), // 形状 ShapeBorder
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
          alignment: Alignment.center, // 在屏幕上的对齐方式
          // 自定义内容
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 16.0),
                child: Text(
                  "提示",
                  style: TextStyle(
                    fontSize: 20,
                    color: Colors.black,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              Text(
                "是否要退出登录,退出登录将清空个人信息",
                style: TextStyle(
                  fontSize: 14,
                ),
              ),
              SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  TextButton(
                    onPressed: () {
                      Navigator.of(context).pop(1);
                    },
                    child: Text("取消"),
                  ),
                  TextButton(
                    onPressed: () {
                      Navigator.of(context).pop(2);
                    },
                    child: Text("确定"),
                  ),
                ],
              ),
            ],
          ),
        );
      },
    );
  }

  Future<int?> _showCustomDialog2(BuildContext context) {
    return showDialog(
      context: context,
      builder: (context) {
        return Dialog(
          backgroundColor: Colors.amber[100],
          elevation: 10,
          insetAnimationCurve: Curves.easeIn,
          insetAnimationDuration: Duration(milliseconds: 200),
          alignment: Alignment.center,
          child: IntrinsicHeight(
            child: Column(
              children: [
                Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(
                    "请选择操作类型",
                    style: TextStyle(
                      color: Colors.blue,
                      fontSize: 20,
                      fontWeight: FontWeight.w400,
                    ),
                  ),
                ),
                SizedBox(
                  height: 300,
                  child: ListView.builder(
                    // shrinkWrap: true,
                    itemBuilder: (context, index) {
                      return ListTile(
                        leading: Icon(Icons.title),
                        title: Text("Action $index"),
                        onTap: () {
                          Navigator.of(context).pop(index);
                        },
                      );
                    },
                    itemExtent: 50,
                    itemCount: 8,
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }

  Future<bool?> _showCupertinoAlertDialog1(BuildContext context) {
    return showCupertinoDialog(
      context: context,
      builder: (context) {
        return CupertinoAlertDialog(
          title: Text("提示"), // 标题
          content: Text("是否要退出登录,退出登录后将清除用户信息?"), // 内容
          insetAnimationDuration: Duration(
              milliseconds: 200), // 动画时间,默认为 const Duration(milliseconds: 100)
          insetAnimationCurve:
              Curves.bounceIn, // 动画效果,渐进渐出等等,默认为 Curves.decelerate
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(false);
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(true);
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );
  }

  Future<int?> _showCupertinoAlertDialog2(BuildContext context) async {
    return showCupertinoDialog(
      context: context,
      builder: (context) {
        return CupertinoAlertDialog(
          title: Text("提示"), // 标题
          content: Text("是否要退出登录,退出登录后将清除用户信息?"), // 内容
          insetAnimationDuration: Duration(
              milliseconds: 200), // 动画时间,默认为 const Duration(milliseconds: 100)
          insetAnimationCurve:
              Curves.bounceIn, // 动画效果,渐进渐出等等,默认为 Curves.decelerate
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(0);
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(1);
              },
              child: Text("确定"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(2);
              },
              child: Text("其他"),
            ),
          ],
        );
      },
    );
  }

  Future<int?> _showCustomDialog3(BuildContext context) {
    return showDialog(
      context: context,
      builder: (ctx) {
        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),
                );
              },
            )),
          ],
        );
        return UnconstrainedBox(
          constrainedAxis: Axis.vertical,
          child: ConstrainedBox(
            constraints: BoxConstraints(maxWidth: 280, maxHeight: 280),
            child: Material(
              child: child,
              type: MaterialType.card,
            ),
          ),
        );
      },
    );
  }

  Future<T?> showCustomGeneralDialog<T>({
    required BuildContext context,
    bool barrierDismissible = true,
    required WidgetBuilder builder,
  }) {
    final ThemeData theme = Theme.of(context);
    return showGeneralDialog(
      context: context,
      pageBuilder: (BuildContext context, Animation<double> animation,
          Animation<double> secondaryAnimation) {
        final Widget pageChild = Builder(builder: builder);
        return SafeArea(
          child: Builder(builder: (context) {
            return theme != null
                ? Theme(data: theme, child: pageChild)
                : pageChild;
          }),
        );
      },
      barrierDismissible: barrierDismissible,
      barrierColor: Colors.black87,
      barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
      transitionDuration: Duration(milliseconds: 150),
      transitionBuilder: (context, animation, secondaryAnimation, child) {
        return ScaleTransition(
          scale: CurvedAnimation(
            parent: animation,
            curve: Curves.easeIn,
          ),
          child: child,
        );
      },
    );
  }

  Future<int?> _showCustomDialog4(BuildContext context) {
    return showCustomGeneralDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("警告"),
          content: Text("您确定要删除当前文件吗"),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(0);
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(1);
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );
  }

  _showCheckBoxDialog1(BuildContext context) {
    bool _withTree = false; //记录复选框是否选中
    return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text("您确定要删除当前文件吗?"),
              Row(
                children: [
                  Text("同时删除子目录?"),
                  StatefulBuilder(
                    builder: (context, _setState) {
                      return Checkbox(
                        value: _withTree,
                        onChanged: (value) {
                          _setState(() {
                            _withTree = value!;
                          });
                        },
                      );
                    },
                  ),
                ],
              )
            ],
          ),
          actions: [
            TextButton(
              child: Text("取消"),
              onPressed: () => Navigator.of(context).pop(),
            ),
            TextButton(
              child: Text("删除"),
              onPressed: () {
                // 将选中状态返回
                Navigator.of(context).pop(_withTree);
              },
            ),
          ],
        );
      },
    );
  }

  _showCheckBoxDialog2(BuildContext context) {
    bool _withTree = false; //记录复选框是否选中
    return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text("您确定要删除当前文件吗?"),
              Row(
                children: [
                  Text("同时删除子目录?"),
                  Checkbox(
                    value: _withTree,
                    onChanged: (value) {
                      // 此时context为对话框UI的根Element,我们
                      // 直接将对话框UI对应的Element标记为dirty
                      (context as Element).markNeedsBuild();
                      _withTree = !_withTree;
                    },
                  ),
                ],
              )
            ],
          ),
          actions: [
            TextButton(
              child: Text("取消"),
              onPressed: () => Navigator.of(context).pop(),
            ),
            TextButton(
              child: Text("删除"),
              onPressed: () {
                // 将选中状态返回
                Navigator.of(context).pop(_withTree);
              },
            ),
          ],
        );
      },
    );
  }

  // 弹出底部菜单列表模态对话框
  Future<int?> _showModalBottomSheet(BuildContext context) {
    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),
            );
          },
        );
      },
    );
  }

  _showLoadingDialog(BuildContext context) {
    showDialog(
      context: context,
      barrierDismissible: true, //点击遮罩关闭对话框
      builder: (context) {
        return AlertDialog(
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              CircularProgressIndicator(),
              Padding(
                padding: const EdgeInsets.only(top: 26.0),
                child: Text("正在加载,请稍后..."),
              )
            ],
          ),
        );
      },
    );
  }

  Future<DateTime?> _showDatePicker1(BuildContext context) {
    var date = DateTime.now();
    return showDatePicker(
      context: context,
      initialDate: date,
      firstDate: date,
      lastDate: date.add(
        //未来30天可选
        Duration(days: 30),
      ),
    );
  }

  Future<DateTime?> _showDatePicker2(BuildContext context) {
    var date = DateTime.now();
    return showCupertinoModalPopup(
      context: context,
      builder: (ctx) {
        return SizedBox(
          height: 200,
          child: CupertinoDatePicker(
            mode: CupertinoDatePickerMode.dateAndTime,
            backgroundColor: Colors.white,
            minimumDate: date,
            maximumDate: date.add(
              Duration(days: 30),
            ),
            maximumYear: date.year + 1,
            onDateTimeChanged: (DateTime value) {
              print(value);
            },
          ),
        );
      },
    );
  }
}

119.gif

https://book.flutterchina.club/chapter7/dailog.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容