基于 flutter_smart_dialog 封装。
如果你习惯了 Android 开发时的体验,喜欢 Material 的设计,那么可以用这个封装好的 AlertDialog,尽可能还原了当初 material_dialogs 的使用习惯。
如果你有更好的建议或实现方式,或者觉得目前的实现有问题,不妨在评论区留下你的意见~
AlertDialog
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
/// show alert dialog
showAlertDialog({
String? title,
String? message,
Widget? content,
String? textInput,
TextInputType? textInputType,
String? textInputHint,
int? textInputMaxLength,
bool cancelable = true,
bool cancelableOutside = true,
bool? secondaryConfirm,
String? secondaryConfirmText,
String? negativeText,
void Function()? onNegative,
String? positiveText,
void Function(dynamic)? onPositive,
List<String>? listItems,
bool? listItemsSingleChoice,
int? listItemsSelection,
bool? listItemMultiChoice,
List<int>? listItemMultiChoiceSelection,
void Function(String text, int index)? onListItemSelected,
void Function(List<String> texts, List<int> indexs)? onListItemsMultiSelected,
}) {
bool containsMessage = message != null || content != null;
bool secondaryConfirmChecked = secondaryConfirm ?? false;
bool containsActionButtons = onNegative != null || onPositive != null || onListItemsMultiSelected != null;
TextEditingController? textFieldController = textInput != null ? TextEditingController() : null;
textFieldController?.text = textInput ?? '';
textFieldController?.value = TextEditingValue(
// 光标位置
text: textInput ?? '',
selection: TextSelection.fromPosition(TextPosition(
affinity: TextAffinity.downstream,
offset: textInput?.length ?? 0,
)),
);
Widget buildTitleWidget() {
assert(title != null);
return Padding(
padding: const EdgeInsets.only(
left: AppDimens.pagePadding,
top: AppDimens.itemPadding,
right: AppDimens.pagePadding,
),
child: Text(
title!,
style: const TextStyle(
fontSize: 22,
),
),
);
}
Widget buildMessageWidget() {
assert(containsMessage);
Widget buildInnerMessageWidget() {
if (content != null) {
return content;
} else {
return Text(message!);
}
}
return Padding(
padding: const EdgeInsets.only(
left: AppDimens.pagePadding,
top: AppDimens.itemPadding,
right: AppDimens.pagePadding,
bottom: AppDimens.itemPadding,
),
child: buildInnerMessageWidget(),
);
}
Widget buildTextInputWidget() {
return Padding(
padding: const EdgeInsets.only(
left: AppDimens.pagePadding,
top: AppDimens.itemPadding,
right: AppDimens.pagePadding,
),
child: TextField(
controller: textFieldController,
keyboardType: textInputType,
textAlign: TextAlign.start,
autofocus: true, // 自动获取焦点
obscureText: false, // 隐藏输入内容
minLines: 1, // 要让 maxLines 生效的同时,让输入框默认显示1行的UI,需要设置 minLines = 1
maxLines: 6,
maxLength: textInputMaxLength ?? 100,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
enabled: true,
decoration: InputDecoration(
// 悬浮 label
labelText: textInputHint,
floatingLabelBehavior: FloatingLabelBehavior.auto,
floatingLabelAlignment: FloatingLabelAlignment.start,
// hint
hintText: textInputHint,
// 背景填充
filled: true,
// 非紧凑的
isDense: false,
enabled: true,
),
),
);
}
Widget buildSecondaryConfirmWidget() {
return Builder(
builder: (context) {
return GestureDetector(
onTap: () {
secondaryConfirmChecked = !secondaryConfirmChecked;
(context as Element).markNeedsBuild();
},
child: Padding(
padding: const EdgeInsets.only(
left: AppDimens.pagePadding,
top: AppDimens.itemPadding,
right: AppDimens.pagePadding,
),
child: CheckboxLabel(
value: secondaryConfirmChecked,
label: secondaryConfirmText ?? 'secondary confirm',
onChanged: (bool? newValue) {
secondaryConfirmChecked = newValue ?? false;
// 标记 Widget 树需要重新 Build,以刷新 UI。
// 该方法来自于:https://book.flutterchina.club/chapter7/dailog.html#_7-7-4-%E5%AF%B9%E8%AF%9D%E6%A1%86%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86
(context as Element).markNeedsBuild();
},
),
),
);
},
);
}
int radioButtonGroupValue = listItemsSelection ?? -1;
List<int> currentMultiSelection = listItemMultiChoiceSelection ?? [];
/// 更新多选选中状态
updateMultiSelection(int index, BuildContext context) {
if (currentMultiSelection.contains(index)) {
currentMultiSelection.remove(index);
} else {
currentMultiSelection.add(index);
}
(context as Element).markNeedsBuild();
}
Widget buildListItemsWidget(BuildContext context) {
assert(listItems != null);
Widget buildItemWidget(int index, String item) {
if (listItemsSingleChoice == true) {
// 单选
return RadioLabel(
value: index,
groupValue: radioButtonGroupValue,
label: item,
onChanged: <int>(value) {
radioButtonGroupValue = value;
(context as Element).markNeedsBuild();
},
);
} else if (listItemMultiChoice == true) {
// 多选
return CheckboxLabel(
value: currentMultiSelection.contains(index),
label: item,
onChanged: (value) {
updateMultiSelection(index, context);
},
);
} else {
// 列表
return Row(
children: [
Expanded(child: Text(item)),
],
);
}
}
// SingleChildScrollView 嵌套在 Column 中,所以使用 Expanded 包裹,否则无效,并且 Expanded 需要在最外层。
return Expanded(
// InWell 水波纹效果需要 Material 组件支持
child: Material(
color: Colors.transparent,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: listItems!.asMap().entries.map(
(entry) {
int index = entry.key;
String item = entry.value;
return InkWell(
borderRadius: BorderRadius.circular(AppDimens.radius), // 水波纹样式
onTap: () {
if (onListItemSelected != null) {
// 单选:list items or single choice
if (listItemsSingleChoice == true) {
// 刷新单选状态UI
radioButtonGroupValue = index;
(context as Element).markNeedsBuild();
}
if (listItemMultiChoice != true) {
// 返回选中
onListItemSelected.call(item, index);
dismissDialog();
} else {
// 更新多选选中状态
updateMultiSelection(index, context);
}
}
},
child: Padding(
padding: const EdgeInsets.only(
top: AppDimens.itemPadding,
bottom: AppDimens.itemPadding,
left: AppDimens.pagePadding,
right: AppDimens.pagePadding,
),
child: buildItemWidget(index, item),
),
);
},
).toList(),
),
),
),
);
}
Widget buildActionButtonsWidget() {
return Padding(
padding: const EdgeInsets.all(AppDimens.itemPadding),
child: OverflowBar(
alignment: MainAxisAlignment.end,
overflowAlignment: OverflowBarAlignment.end,
overflowDirection: VerticalDirection.down,
spacing: 0, // 未 overflow 时按钮之间的间距
overflowSpacing: 0, // 如果 overflow 了,overflow 后按钮之间的间距
children: [
// 使用 TextButton,由于存在点击效果,所以默认显示时看上去有 padding 的样子,所以我们不需要再设置 overflow 的 spacing
if (onNegative != null) ...[
TextButton(
child: Text(negativeText ?? S.current.cancel),
onPressed: () {
SmartDialog.dismiss();
onNegative.call();
},
),
],
if (onPositive != null || onListItemsMultiSelected != null) ...[
TextButton(
child: Text(positiveText ?? S.current.confirm),
onPressed: () {
SmartDialog.dismiss();
onPositive?.call(textFieldController?.text);
// 返回多选选择
onListItemsMultiSelected?.call(
listItems!
.asMap()
.entries
.where((entry) => currentMultiSelection.contains(entry.key))
.map((entry) => entry.value)
.toList(),
currentMultiSelection..sort((a, b) => a.compareTo(b)),
);
},
),
]
],
),
);
}
// show dialog
SmartDialog.show(
builder: (context) {
return Padding(
// 设置 dialog 距离屏幕的 padding,这里设置的值取自 Flutter Dialog 内部的实现
padding: MediaQuery.viewInsetsOf(context) +
const EdgeInsets.symmetric(
horizontal: 46.0, // Flutter 默认40,横屏时靠近状态栏太近了,所以我们改大点
vertical: 64.0, // Flutter 默认24,顶部可能会超出到状态栏,所以我们改大点
),
child: ConstrainedBox(
// 设置 Dialog 最小宽度280,取自 Flutter Dialog 内部的实现
constraints: const BoxConstraints(
minWidth: 280,
),
// 背景
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppDimens.radius), // 圆角
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, // Column 高度自适应
children: [
// title
if (title != null) buildTitleWidget(),
// message
if (containsMessage) buildMessageWidget(),
// text input
if (textInput != null) buildTextInputWidget(),
// secondary check box
if (secondaryConfirm != null) buildSecondaryConfirmWidget(),
// list items options
if (listItems != null) buildListItemsWidget(context),
// action buttons
if (containsActionButtons) buildActionButtonsWidget(),
],
),
),
),
);
},
// Dialog 显示位置
alignment: Alignment.center,
// 点击外部空白处是否可以关闭
clickMaskDismiss: cancelableOutside,
// 点击穿透
usePenetrate: false,
// 动画
useAnimation: true,
animationType: SmartAnimationType.centerFade_otherSlide,
animationTime: const Duration(milliseconds: 220),
// 防抖
debounce: true,
// true: 多次调用 show,不产生多个 Dialog;false:产生多个 Dialog
keepSingle: false,
// Dialog 关闭时的回调
onDismiss: () {},
// 不使用系统风格的 Dialog
useSystem: false,
// 拦截返回键类型:(normal)触发返回按键时,自动关闭 Dialog,(block)触发返回按键时,拦截不做任何处理
backType: cancelable ? SmartBackType.normal : SmartBackType.block,
);
}
使用
showAlertDialog(
content: const Text("message"),
onPositive: (v) {},
);