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

相关推荐
liulian091611 分钟前
Flutter for OpenHarmony 跨平台开发:单位转换功能实战指南
flutter
千码君20161 小时前
Trae:一些关于flutter和 go前后端开发构建的分享
android·flutter·gradle·android-studio·trae·vibe code
maaath2 小时前
【maaath】Flutter for OpenHarmony 手表配饰应用实战开发
flutter·华为·harmonyos
maaath3 小时前
【maaath】Flutter for OpenHarmony 跨平台计算器应用开发实践
flutter·华为·harmonyos
maaath8 小时前
【maaath】Flutter for OpenHarmony 闹钟时钟应用开发实战
flutter·华为·harmonyos
maaath9 小时前
【maaath】Flutter for OpenHarmony 短信管理应用实战
flutter·华为·harmonyos
maaath9 小时前
【maaath】Flutter for OpenHarmony打造跨平台便签备忘录应用
flutter·华为·harmonyos
千码君201610 小时前
flutter:与Android Studio模拟器的调试分享
android·flutter
xmdy586610 小时前
Flutter+开源鸿蒙实战|智联邻里Day8 Lottie动画集成+url_launcher跳转拨号+个人中心完善+全局UI统一
flutter·开源·harmonyos
liulian091618 小时前
Flutter for OpenHarmony 跨平台开发:颜色选择器功能实战指南
flutter