基于 modal_bottom_sheet 封装。
如果你习惯了 Android 开发时的体验,喜欢 Material 的设计,那么可以用这个封装好的 BottomSheet,尽可能还原了当初 material_dialogs 的使用习惯。
如果你有更好的建议或实现方式,或者觉得目前的实现有问题,不妨在评论区留下你的意见~
BottomSheet
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
/// 显示 Bottomsheet
showBottomSheetDialog({
required BuildContext context,
String? title,
String? message,
Widget? content,
Widget? list,
bool? fullscreen,
bool dragHandler = true,
List<BottomSheetTitleAction>? titleActions,
bool cancelable = true,
bool cancelableOutside = true,
bool? secondaryConfirm,
String? secondaryConfirmText,
String? negativeText,
void Function()? onNegative,
String? positiveText,
void Function(dynamic)? onPositive,
}) {
bool containsMessage = message != null || content != null;
bool secondaryConfirmChecked = secondaryConfirm ?? false;
bool containsActionButtons = onNegative != null || onPositive != null;
bool containsTitleActions = titleActions != null;
bool containsTitle = title != null || containsTitleActions;
Widget buildDragHandler() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 12),
child: SizedBox(
width: 40,
height: 5,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
);
}
Widget buildTitleWidget() {
assert(title != null);
return Text(
title!,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeightExt.semibold,
),
);
}
Widget buildMessageWidget() {
assert(containsMessage);
Widget buildInnerMessageWidget() {
if (content != null) {
return SingleChildScrollView(child: SizedBox(width: double.infinity, child: content));
} else {
return SingleChildScrollView(child: Text(message!));
}
}
return Flexible(
child: Padding(
padding: const EdgeInsets.only(
left: AppDimens.pagePadding,
top: AppDimens.itemPadding,
right: AppDimens.pagePadding,
bottom: AppDimens.itemPadding,
),
child: buildInnerMessageWidget(),
),
);
}
Widget buildTitleActionsWidget(BottomSheetTitleAction titleAction) {
assert(containsTitleActions);
// load from Icons
if (titleAction.iconData != null) {
return IconButton(
onPressed: () => titleAction.onTap,
icon: Icon(titleAction.iconData),
);
}
// load from text
if (!titleAction.name.isNullOrBlank) {
return TextButton(
onPressed: () => titleAction.onTap,
child: Text(titleAction.name!),
);
}
// load from custom icon
return const Text("placeholder");
}
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.markRebuildUI();
},
),
),
);
},
);
}
double sheetMaxHeight;
if (fullscreen == true) {
sheetMaxHeight = double.infinity;
} else {
if (containsMessage && list != null) {
sheetMaxHeight = double.infinity;
} else {
sheetMaxHeight = MediaQuery.of(context).size.height / 1.5;
}
}
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: () {
dismissBottomSheet(context);
onNegative.call();
},
),
],
if (onPositive != null) ...[
TextButton(
child: Text(positiveText ?? S.current.confirm),
onPressed: () {
dismissBottomSheet(context);
onPositive.call(null);
},
),
]
],
),
);
}
// show bottom sheet
return showMaterialModalBottomSheet(
context: context,
/// true 全屏(忽略设置的高度);false自适应高度
expand: fullscreen ?? false,
/// 点击 BottomSheet 外部区域能否关闭 BottomSheet
isDismissible: cancelableOutside,
/// 是否可以拖拽,并且是否可以通过拖拽来关闭 BottomSheet
enableDrag: cancelable,
/// 指定 BottomSheet 在拖动时是否可以超出顶部边界
bounce: true,
/// 当用户拖动 BottomSheet 时,何时关闭 BottomSheet
closeProgressThreshold: 0.6,
// 背景
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(AppDimens.radius),
topRight: Radius.circular(AppDimens.radius),
),
),
clipBehavior: Clip.antiAlias,
/// bottom sheet content
builder: (context) {
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: sheetMaxHeight),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, // Column 高度自适应
children: [
// drag handler
if (dragHandler) buildDragHandler(),
// title
if (containsTitle)
Padding(
padding: const EdgeInsets.only(
left: AppDimens.pagePadding,
top: AppDimens.itemPadding,
right: 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// title
if (!title.isNullOrBlank) buildTitleWidget(),
const Spacer(),
// title actions
if (containsTitleActions)
...titleActions.map((titleAction) => buildTitleActionsWidget(titleAction)),
],
),
),
// message
if (containsMessage) buildMessageWidget(),
// list view
if (list != null) Flexible(child: list),
// secondary confirm checkbox
if (secondaryConfirm != null) buildSecondaryConfirmWidget(),
// action buttons
if (containsActionButtons) buildActionButtonsWidget(),
// bottom padding
SizedBox(height: containsActionButtons ? 18 : 38),
],
),
);
},
);
}
dismissBottomSheet(BuildContext context) {
Navigator.of(context).pop();
}
class BottomSheetTitleAction {
String? name;
String? icon;
IconData? iconData;
VoidCallback? onTap;
BottomSheetTitleAction({
this.name,
this.icon,
this.iconData,
this.onTap,
});
}
class TestText extends StatefulWidget {
const TestText({super.key, required this.text});
final String text;
@override
State<TestText> createState() => _TestTextState();
}
class _TestTextState extends State<TestText> {
@override
void initState() {
super.initState();
print("init widget: ${widget.text}");
}
@override
void dispose() {
super.dispose();
print("dispose: ${widget.text}");
}
@override
Widget build(BuildContext context) {
return Text(widget.text);
}
}
使用
showBottomSheetDialog(
context: context,
title: "Bottom Sheet",
titleActions: [
BottomSheetTitleAction(name: "Add", iconData: Icons.add),
BottomSheetTitleAction(name: "Done", iconData: Icons.done),
],
// message: "Hhhh",
// content: const Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // SingleChildScrollView 会一次性加载所有
// TestText(text: "kkk"),
// TestText(text: "kkk"),
// TestText(text: "kkk"),
// ],
// ),
list: ListView.builder(
itemCount: 320,
padding: const EdgeInsets.only(top: 0),
itemBuilder: (BuildContext context, int index) {
// List.builder 会滚动加载
return ListTile(
title: TestText(text: "${index + 1}"),
onTap: () => Navigator.of(context).pop(index),
);
},
),
secondaryConfirm: true,
secondaryConfirmText: "Are you ok?",
onNegative: () => print("negative"),
onPositive: (v) => print("positive"),
);