对话框本质上也是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("切换语言:英式英语");
}
});
}
}
列表项组件我们使用了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");
}
}
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");
}
}
示例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);
},
),
],
);
},
);
}
}
注意:如果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("确定")),
],
),
],
),
);
},
);
}
}
示例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,
),
),
],
),
),
);
},
);
}
}
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 ? '确定' : '取消'}");
}
}
示例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 ? '确定' : '取消'}");
}
}
}
注意:使用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");
}
}
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,
),
),
);
},
);
}
}
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("确定"),
),
],
);
},
);
}
}
可以发现,遮罩颜色比通过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. 对话框状态管理
我们在用户选择删除一个文件时,会询问是否删除此文件;在用户选择一个文件夹时,应该再让用户确认是否删除子文件夹。为了在用户选择了文件夹时避免二次弹窗确认是否删除子目录,我们在确认对话框底部添加一个“同时删除子目录?”的复选框。
现在就有一个问题:如何管理复选框的选中状态?习惯上,我们会在路由页的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,但是我们的对话框并不是在_MSDialogStatusDemo1State
的build
方法中构建的,而是通过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);
});
},
);
}
}
可见复选框能选中了,点击“取消”或“删除”后,控制台就会打印出最终的确认状态。
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);
},
),
运行后效果如下所示:
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所示:
如果我们嫌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所示:
7.3 日历选择器
我们先看一下Material风格的日历选择器,如图7-20所示:
实现代码
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所示:
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,
),
);
},
),
),
);
}
}
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);
},
),
);
},
);
}
}