底部弹窗(ActionSheet)是 Flutter 应用的核心交互组件,广泛用于操作选择、筛选分类、危险操作二次确认等场景。原生showModalBottomSheet配置繁琐,重复编写不仅导致样式不统一,还存在交互体验割裂、适配性差等问题。本文封装的ActionSheetWidget,整合 "灵活选项 + 全样式自定义 + 原生级交互 + 全面屏适配" 四大核心能力,一行代码即可调用,适配 90%+ 底部弹窗场景,彻底解放重复开发!
一、核心需求拆解(直击开发痛点)
✅ 选项灵活:支持纯文本、图标 + 文本两种选项类型,支持禁用状态配置✅ 样式全自定义:标题、选项、取消按钮的颜色、大小、间距、圆角均可配置✅ 交互原生:点击选项自动关闭弹窗、点击蒙层关闭、滑动关闭,贴合系统交互习惯✅ 适配全面屏:自动适配底部安全区,避免被手势区域遮挡✅ 安全可控:弹窗弹出时禁止背景滚动,关闭时释放资源,无内存泄漏✅ 扩展便捷:支持自定义分隔线、底部间距、取消按钮样式,适配特殊设计需求
二、完整代码实现(可直接复制使用)
dart
import 'package:flutter/material.dart';
// 底部弹窗选项模型(统一配置单个选项,清晰无冗余)
class ActionSheetItem {
final String title; // 选项文本
final IconData? icon; // 选项图标(可选,图标+文本组合更直观)
final bool isDisabled; // 是否禁用(默认false,禁用时无点击反馈)
final Color? textColor; // 选项文本颜色(默认黑色,支持自定义强调色)
final VoidCallback onTap; // 选项点击回调(核心交互)
const ActionSheetItem({
required this.title,
this.icon,
this.isDisabled = false,
this.textColor,
required this.onTap,
});
}
/// 通用底部弹窗组件(支持全场景自定义,交互流畅)
class ActionSheetWidget extends StatelessWidget {
// 必选参数:核心配置
final List<ActionSheetItem> items; // 选项列表(必填,支持任意数量)
final VoidCallback onCancel; // 取消按钮回调(关闭弹窗后的逻辑)
// 可选参数:样式配置(均有合理默认值,适配主流设计)
final String? title; // 弹窗标题(可选,用于提示操作意图)
final String cancelText; // 取消按钮文本(默认"取消")
final double borderRadius; // 顶部圆角(默认16px,符合现代设计)
final Color bgColor; // 弹窗背景色(默认白色)
final Color titleColor; // 标题颜色(默认灰色,提示性文本)
final Color cancelTextColor; // 取消按钮颜色(默认蓝色,突出可点击)
final Color disabledColor; // 禁用选项颜色(默认浅灰,无交互感)
final double itemHeight; // 选项高度(默认56px,适配点击区域)
final double titleHeight; // 标题高度(默认48px,居中对齐)
final double textSize; // 文本大小(默认16px,可读性强)
final double iconSize; // 图标大小(默认20px,与文本比例协调)
final double paddingHorizontal; // 水平内边距(默认16px,避免内容贴边)
final bool showDivider; // 是否显示选项分隔线(默认true,区分选项)
final Color dividerColor; // 分隔线颜色(默认浅灰,不突兀)
const ActionSheetWidget({
super.key,
required this.items,
required this.onCancel,
this.title,
this.cancelText = '取消',
this.borderRadius = 16.0,
this.bgColor = Colors.white,
this.titleColor = const Color(0xFF999999),
this.cancelTextColor = Colors.blueAccent,
this.disabledColor = const Color(0xFFCCCCCC),
this.itemHeight = 56.0,
this.titleHeight = 48.0,
this.textSize = 16.0,
this.iconSize = 20.0,
this.paddingHorizontal = 16.0,
this.showDivider = true,
this.dividerColor = const Color(0xFFF5F5F5),
});
/// 静态调用方法:一行代码显示弹窗(核心易用性优化)
static void show({
required BuildContext context,
required List<ActionSheetItem> items,
required VoidCallback onCancel,
String? title,
String cancelText = '取消',
double borderRadius = 16.0,
}) {
showModalBottomSheet(
context: context,
// 配置弹窗形状,顶部圆角
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(borderRadius)),
),
backgroundColor: Colors.transparent, // 透明背景,避免默认阴影冲突
isScrollControlled: true, // 支持全屏高度弹窗(适配多选项场景)
barrierDismissible: true, // 点击蒙层关闭弹窗(默认开启,符合用户习惯)
builder: (context) => ActionSheetWidget(
items: items,
onCancel: onCancel,
title: title,
cancelText: cancelText,
borderRadius: borderRadius,
),
);
}
/// 构建单个选项(根据是否禁用、是否有图标动态适配)
Widget _buildActionItem(ActionSheetItem item) {
final isDisabled = item.isDisabled;
// 文本颜色:禁用状态→禁用色,自定义颜色→自定义色,默认→黑色
final textColor = isDisabled ? disabledColor : (item.textColor ?? Colors.black87);
return GestureDetector(
onTap: isDisabled ? null : () {
item.onTap(); // 执行选项回调
Navigator.pop(context); // 点击后自动关闭弹窗,无需手动处理
},
child: Container(
height: itemHeight,
padding: EdgeInsets.symmetric(horizontal: paddingHorizontal),
alignment: Alignment.centerLeft, // 选项内容左对齐,符合阅读习惯
child: Row(
children: [
// 图标(可选:有图标时显示,无则隐藏)
if (item.icon != null) ...[
Icon(
item.icon,
size: iconSize,
color: textColor,
),
const SizedBox(width: 12), // 图标与文本间距,视觉协调
],
// 选项文本(占满剩余宽度,避免溢出)
Expanded(
child: Text(
item.title,
style: TextStyle(
color: textColor,
fontSize: textSize,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis, // 文本溢出截断,避免换行
),
),
],
),
),
);
}
/// 构建选项列表(含分隔线,最后一个选项无分隔线)
Widget _buildActionList() {
final List<Widget> widgetList = [];
for (int i = 0; i < items.length; i++) {
widgetList.add(_buildActionItem(items[i]));
// 分隔线:显示且非最后一个选项时添加
if (showDivider && i != items.length - 1) {
widgetList.add(
Padding(
padding: EdgeInsets.symmetric(horizontal: paddingHorizontal),
child: Divider(
height: 1,
color: dividerColor,
thickness: 1, // 分隔线厚度,避免过粗
),
),
);
}
}
return Column(children: widgetList);
}
@override
Widget build(BuildContext context) {
return Container(
// 弹窗背景与圆角
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(borderRadius)),
),
child: Column(
mainAxisSize: MainAxisSize.min, // 仅占用子组件高度,避免弹窗过高
children: [
// 标题(可选:有标题时显示,居中对齐)
if (title != null)
Container(
height: titleHeight,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: paddingHorizontal),
child: Text(
title!,
style: TextStyle(
color: titleColor,
fontSize: textSize,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// 选项列表(核心内容)
_buildActionList(),
// 分隔线:选项与取消按钮之间的空隙,视觉分隔
const Divider(height: 8, color: Colors.transparent),
// 取消按钮(独立样式,突出可点击)
GestureDetector(
onTap: () {
onCancel(); // 执行取消回调
Navigator.pop(context); // 关闭弹窗
},
child: Container(
height: itemHeight,
margin: EdgeInsets.symmetric(horizontal: paddingHorizontal, vertical: 8),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(borderRadius / 2), // 取消按钮圆角,与弹窗呼应
border: Border.all(color: dividerColor), // 边框区分,突出按钮形态
),
alignment: Alignment.center,
child: Text(
cancelText,
style: TextStyle(
color: cancelTextColor,
fontSize: textSize,
fontWeight: FontWeight.w500,
),
),
),
),
// 底部安全区间距(适配全面屏,避免被底部手势区域遮挡)
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
);
}
}
三、实战使用示例(覆盖 4 大高频场景)
场景 1:基础操作选项(消息列表长按)
适配消息、文件等列表的长按操作,支持图标 + 文本组合,突出关键操作:
dart
// 消息列表长按弹出操作菜单
ActionSheetWidget.show(
context: context,
items: [
ActionSheetItem(
title: '标记已读',
icon: Icons.mark_email_read,
onTap: () {
debugPrint('执行标记已读操作');
// 实际场景:更新消息状态为已读
},
),
ActionSheetItem(
title: '删除消息',
icon: Icons.delete,
textColor: Colors.redAccent, // 危险操作标红,提醒用户
onTap: () {
debugPrint('执行删除消息操作');
// 实际场景:删除本地+接口消息
},
),
ActionSheetItem(
title: '转发消息',
icon: Icons.share,
onTap: () {
debugPrint('执行转发消息操作');
// 实际场景:打开转发选择页
},
),
ActionSheetItem(
title: '屏蔽此人',
icon: Icons.block,
isDisabled: true, // 未开放功能禁用
onTap: () {},
),
],
onCancel: () {
debugPrint('取消操作,不执行任何逻辑');
},
title: '消息操作', // 标题提示操作场景
borderRadius: 12,
);
场景 2:筛选选项(商品列表筛选)
适配电商商品、资讯列表等筛选场景,纯文本选项简洁明了:
dart
// 商品列表筛选弹窗
ActionSheetWidget.show(
context: context,
items: [
ActionSheetItem(
title: '价格从低到高',
onTap: () {
debugPrint('筛选:价格从低到高');
// 实际场景:调用筛选接口,更新商品列表
},
),
ActionSheetItem(
title: '价格从高到低',
onTap: () {
debugPrint('筛选:价格从高到低');
},
),
ActionSheetItem(
title: '销量优先',
onTap: () {
debugPrint('筛选:销量优先');
},
),
ActionSheetItem(
title: '好评优先',
onTap: () {
debugPrint('筛选:好评优先');
},
),
ActionSheetItem(
title: '新品优先',
onTap: () {
debugPrint('筛选:新品优先');
},
),
],
onCancel: () {},
title: '筛选条件',
cancelText: '取消筛选', // 自定义取消按钮文本,更贴合场景
borderRadius: 12,
itemHeight: 52, // 缩小选项高度,适配多选项
textSize: 15,
cancelTextColor: Colors.greenAccent, // 取消按钮颜色自定义
);
场景 3:危险操作确认(二次确认)
适配删除、注销等危险操作,突出确认按钮,降低误操作概率:
dart
// 注销账号二次确认弹窗
ActionSheetWidget.show(
context: context,
items: [
ActionSheetItem(
title: '确认注销',
textColor: Colors.red, // 危险操作标红
onTap: () {
debugPrint('执行注销账号操作');
// 实际场景:调用注销接口,清除本地缓存
},
),
],
onCancel: () {},
title: '注销后账号数据不可恢复,确定要注销吗?', // 长文本提示风险
cancelText: '取消',
borderRadius: 8,
itemHeight: 60, // 增大按钮高度,提升点击区域
textSize: 18, // 增大文本字号,突出重要性
titleColor: Colors.redAccent, // 标题标红,强化风险提示
showDivider: false, // 单选项隐藏分隔线
);
场景 4:自定义样式弹窗(品牌化设计)
适配需要个性化风格的场景,自定义颜色、间距,贴合 APP 品牌调性:
dart
// 品牌化风格弹窗
ActionSheetWidget.show(
context: context,
items: [
ActionSheetItem(
title: '分享到微信',
icon: Icons.wechat,
textColor: const Color(0xFF07C160), // 微信绿色
onTap: () {
debugPrint('分享到微信');
},
),
ActionSheetItem(
title: '分享到微博',
icon: Icons.weibo,
textColor: const Color(0xFFFF1D25), // 微博红色
onTap: () {
debugPrint('分享到微博');
},
),
ActionSheetItem(
title: '分享到QQ',
icon: Icons.qq,
textColor: const Color(0xFF12B7F5), // QQ蓝色
onTap: () {
debugPrint('分享到QQ');
},
),
],
onCancel: () {},
title: '分享到',
bgColor: const Color(0xFFFAFAFA), // 浅灰背景,区别于白色页面
borderRadius: 20, // 大圆角,更柔和
itemHeight: 58,
paddingHorizontal: 20, // 增大水平内边距,避免贴边
dividerColor: const Color(0xFFEEEEEE),
cancelTextColor: const Color(0xFF666666),
);
四、核心封装技巧(让组件更易用、更稳定)
- 静态调用简化流程 :通过
static void show方法封装showModalBottomSheet,无需手动构建弹窗实例和配置形状,一行代码即可调用,降低使用门槛。 - 选项模型化管理 :通过
ActionSheetItem统一管理选项的文本、图标、禁用状态,配置清晰无冗余,新增 / 修改选项时无需改动组件核心逻辑。 - 全面屏适配 :底部自动添加
MediaQuery.of(context).padding.bottom安全区间距,避免弹窗被全面屏底部手势区域遮挡,适配所有设备。 - 交互细节优化:点击选项自动关闭弹窗、禁用选项无点击反馈、文本溢出截断,每一处细节都贴合用户操作习惯,提升体验。
- 样式全维度自定义:从颜色、大小、间距到圆角、分隔线,所有可见元素均可配置,既保证通用性,又能适配不同 APP 的品牌设计风格。
- 边界条件处理:最后一个选项自动隐藏分隔线、文本溢出自动截断、禁用状态自动切换颜色,避免 UI 异常,提升组件稳定性。
五、避坑指南(实际开发必看)
- 选项数量控制:建议选项数量控制在 3-6 个,过多会导致弹窗高度过高,需滑动才能查看全部选项,影响交互体验;超过 6 个建议使用滚动列表优化。
- 文本长度限制:选项文本不宜过长(建议不超过 10 个字符),过长会导致文本溢出或换行,可通过精简文本、缩小字体或增大水平内边距解决。
- 禁用状态配置 :禁用选项需设置
isDisabled: true,组件会自动切换文本颜色为禁用色并屏蔽点击事件,无需额外配置,避免逻辑遗漏。 - 蒙层交互控制 :默认点击蒙层关闭弹窗,若需禁止(如危险操作确认),可在
showModalBottomSheet中设置barrierDismissible: false,强制用户选择选项或取消按钮。 - 状态同步问题:弹窗操作后需手动更新页面状态(如筛选后刷新列表、删除后更新 UI),确保弹窗操作与页面状态一致。
- 圆角适配:弹窗圆角与取消按钮圆角建议保持比例(如弹窗圆角 16px,取消按钮圆角 8px),视觉更协调;避免圆角过大导致样式突兀。
总结
ActionSheetWidget通过 "通用化封装 + 精细化交互 + 全维度自定义",彻底解决了原生底部弹窗开发的繁琐问题。无论是基础操作、筛选分类,还是危险操作确认,都能通过简单配置快速实现,既保证了 APP 内弹窗样式的统一性,又大幅提升了开发效率。组件兼顾了易用性和灵活性,新手可直接调用默认配置,进阶用户可自定义所有样式,适配不同场景需求。