Flutter 通用底部弹窗:ActionSheetWidget 一键实现自定义选项与交互

底部弹窗(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),
);

四、核心封装技巧(让组件更易用、更稳定)

  1. 静态调用简化流程 :通过static void show方法封装showModalBottomSheet,无需手动构建弹窗实例和配置形状,一行代码即可调用,降低使用门槛。
  2. 选项模型化管理 :通过ActionSheetItem统一管理选项的文本、图标、禁用状态,配置清晰无冗余,新增 / 修改选项时无需改动组件核心逻辑。
  3. 全面屏适配 :底部自动添加MediaQuery.of(context).padding.bottom安全区间距,避免弹窗被全面屏底部手势区域遮挡,适配所有设备。
  4. 交互细节优化:点击选项自动关闭弹窗、禁用选项无点击反馈、文本溢出截断,每一处细节都贴合用户操作习惯,提升体验。
  5. 样式全维度自定义:从颜色、大小、间距到圆角、分隔线,所有可见元素均可配置,既保证通用性,又能适配不同 APP 的品牌设计风格。
  6. 边界条件处理:最后一个选项自动隐藏分隔线、文本溢出自动截断、禁用状态自动切换颜色,避免 UI 异常,提升组件稳定性。

五、避坑指南(实际开发必看)

  1. 选项数量控制:建议选项数量控制在 3-6 个,过多会导致弹窗高度过高,需滑动才能查看全部选项,影响交互体验;超过 6 个建议使用滚动列表优化。
  2. 文本长度限制:选项文本不宜过长(建议不超过 10 个字符),过长会导致文本溢出或换行,可通过精简文本、缩小字体或增大水平内边距解决。
  3. 禁用状态配置 :禁用选项需设置isDisabled: true,组件会自动切换文本颜色为禁用色并屏蔽点击事件,无需额外配置,避免逻辑遗漏。
  4. 蒙层交互控制 :默认点击蒙层关闭弹窗,若需禁止(如危险操作确认),可在showModalBottomSheet中设置barrierDismissible: false,强制用户选择选项或取消按钮。
  5. 状态同步问题:弹窗操作后需手动更新页面状态(如筛选后刷新列表、删除后更新 UI),确保弹窗操作与页面状态一致。
  6. 圆角适配:弹窗圆角与取消按钮圆角建议保持比例(如弹窗圆角 16px,取消按钮圆角 8px),视觉更协调;避免圆角过大导致样式突兀。

总结

ActionSheetWidget通过 "通用化封装 + 精细化交互 + 全维度自定义",彻底解决了原生底部弹窗开发的繁琐问题。无论是基础操作、筛选分类,还是危险操作确认,都能通过简单配置快速实现,既保证了 APP 内弹窗样式的统一性,又大幅提升了开发效率。组件兼顾了易用性和灵活性,新手可直接调用默认配置,进阶用户可自定义所有样式,适配不同场景需求。

https://openharmonycrossplatform.csdn.net/content

相关推荐
小a彤2 小时前
Flutter 深度解析:跨平台开发的终极利器
flutter
_大学牲2 小时前
Flutter 勇闯2D像素游戏之路(二):绘制加载游戏地图
flutter·游戏·游戏开发
程序员老刘2 小时前
千万别再纠结Flutter状态管理,90%项目根本不需要选
flutter·客户端
renxhui3 小时前
Flutter 常用组件全属性说明(持续更新中)
flutter
m0_看见流浪猫请投喂4 小时前
Flutter鸿蒙化现有三方插件兼容适配鸿蒙平台
flutter·华为·harmonyos·flutterplugin·flutter鸿蒙化
雨季6664 小时前
Flutter 智慧物流仓储服务平台:跨端协同打造高效流转生态
flutter
勇气要爆发4 小时前
【第五阶段—高级特性和框架】第十一章:Flutter屏幕适配开发技巧—变形秘籍
flutter
吃好喝好玩好睡好5 小时前
Flutter与Electron在OpenHarmony生态的融合实践:构建下一代跨平台应用
javascript·flutter·electron
ujainu5 小时前
Flutter:在平台博弈中构建跨端开发新生态
flutter