Flutter iOS 风格弹框组件封装

📱 概述

在 Flutter 开发中,为了给 iOS 用户提供原生般的体验,使用 Cupertino 风格的弹框至关重要。本文将介绍如何封装一套完整、易用的 iOS 风格弹框组件库,涵盖日常开发中所需的所有弹框类型。



📁 项目结构

复制代码
lib/
├── ios_dialog.dart          # iOS 弹框主类
├── ios_dialog_example.dart  # 使用示例
└── widgets/                 # 相关组件

🔧 核心实现

ios_dialog.dart

dart 复制代码
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

/// iOS 风格弹框工具类
/// 提供完整的 iOS 原生体验弹框组件
class IosDialog {
  // ================================
  // 1. 警告弹框 (Alert)
  // ================================
  
  /// 基础警告弹框
  /// 
  /// 参数说明:
  /// - title: 标题(必需)
  /// - message: 消息内容(可选)
  /// - actions: 自定义按钮(可选,默认提供确定按钮)
  static Future<T?> showAlert<T>({
    required BuildContext context,
    required String title,
    String? message,
    List<CupertinoDialogAction>? actions,
    bool barrierDismissible = true,
  }) {
    return showCupertinoDialog<T>(
      context: context,
      barrierDismissible: barrierDismissible,
      builder: (context) => CupertinoAlertDialog(
        title: Text(
          title,
          style: const TextStyle(
            fontWeight: FontWeight.w600,
            fontSize: 17,
          ),
        ),
        content: message != null ? Text(message) : null,
        actions: actions ?? [
          CupertinoDialogAction(
            isDefaultAction: true,
            child: const Text('确定'),
            onPressed: () => Navigator.of(context).pop(),
          ),
        ],
      ),
    );
  }

  /// 确认弹框
  /// 
  /// 用于需要用户确认的操作,返回 true/false
  static Future<bool?> showConfirm({
    required BuildContext context,
    required String title,
    String? message,
    String confirmText = '确定',
    String cancelText = '取消',
    bool destructive = false,
  }) {
    return showCupertinoDialog<bool>(
      context: context,
      builder: (context) => CupertinoAlertDialog(
        title: Text(title),
        content: message != null ? Text(message) : null,
        actions: [
          CupertinoDialogAction(
            child: Text(cancelText),
            onPressed: () => Navigator.of(context).pop(false),
          ),
          CupertinoDialogAction(
            isDefaultAction: true,
            isDestructiveAction: destructive,
            child: Text(confirmText),
            onPressed: () => Navigator.of(context).pop(true),
          ),
        ],
      ),
    );
  }

  /// 多个选项的选择弹框
  static Future<int?> showChoice({
    required BuildContext context,
    required String title,
    String? message,
    required List<String> options,
    String cancelText = '取消',
  }) {
    return showCupertinoDialog<int>(
      context: context,
      builder: (context) => CupertinoAlertDialog(
        title: Text(title),
        content: message != null ? Text(message) : null,
        actions: [
          for (var i = 0; i < options.length; i++)
            CupertinoDialogAction(
              child: Text(options[i]),
              onPressed: () => Navigator.of(context).pop(i),
            ),
          CupertinoDialogAction(
            child: Text(cancelText),
            onPressed: () => Navigator.of(context).pop(),
          ),
        ],
      ),
    );
  }

  // ================================
  // 2. 底部操作菜单 (ActionSheet)
  // ================================
  
  /// 基础底部操作菜单
  static Future<T?> showActionSheet<T>({
    required BuildContext context,
    String? title,
    String? message,
    String? cancelText,
    required List<CupertinoActionSheetAction> actions,
    bool showCancel = true,
  }) {
    return showCupertinoModalPopup<T>(
      context: context,
      semanticsDismissible: true,
      builder: (context) => CupertinoActionSheet(
        title: title != null
            ? Text(
                title,
                style: const TextStyle(
                  fontWeight: FontWeight.w600,
                  fontSize: 13,
                  color: CupertinoColors.secondaryLabel,
                ),
              )
            : null,
        message: message != null
            ? Text(
                message,
                style: const TextStyle(
                  fontSize: 13,
                  color: CupertinoColors.secondaryLabel,
                ),
              )
            : null,
        actions: actions,
        cancelButton: showCancel
            ? CupertinoActionSheetAction(
                isDefaultAction: true,
                onPressed: () => Navigator.of(context).pop(),
                child: Text(
                  cancelText ?? '取消',
                  style: const TextStyle(
                    fontWeight: FontWeight.w600,
                    fontSize: 20,
                  ),
                ),
              )
            : null,
      ),
    );
  }

  /// 快捷创建操作菜单
  static Future<void> showSimpleActionSheet({
    required BuildContext context,
    required String title,
    required List<ActionSheetItem> items,
    String? cancelText,
  }) {
    final actions = items.map((item) {
      return CupertinoActionSheetAction(
        onPressed: () {
          Navigator.of(context).pop();
          item.onPressed?.call();
        },
        child: Text(
          item.title,
          style: item.destructive == true
              ? const TextStyle(color: CupertinoColors.systemRed)
              : null,
        ),
      );
    }).toList();

    return showActionSheet(
      context: context,
      title: title,
      actions: actions,
      cancelText: cancelText,
    );
  }

  // ================================
  // 3. 选择器 (Picker)
  // ================================
  
  /// 列表选择器
  static Future<T?> showPicker<T>({
    required BuildContext context,
    required List<T> items,
    required String Function(T) itemBuilder,
    T? initialItem,
    String title = '请选择',
    String confirmText = '完成',
    String cancelText = '取消',
  }) {
    T? selectedItem = initialItem ?? items.first;

    return showCupertinoModalPopup<T>(
      context: context,
      builder: (context) => Container(
        height: 250,
        color: CupertinoColors.systemBackground.resolveFrom(context),
        child: Column(
          children: [
            // 顶部工具栏
            _buildPickerToolbar(
              context: context,
              title: title,
              confirmText: confirmText,
              cancelText: cancelText,
              onConfirm: () => Navigator.of(context).pop(selectedItem),
              onCancel: () => Navigator.of(context).pop(),
            ),
            
            // 选择器主体
            Expanded(
              child: CupertinoPicker(
                scrollController: FixedExtentScrollController(
                  initialItem: items.indexOf(selectedItem!),
                ),
                itemExtent: 40,
                onSelectedItemChanged: (index) {
                  selectedItem = items[index];
                },
                children: items.map((item) {
                  return Center(
                    child: Text(
                      itemBuilder(item),
                      style: const TextStyle(fontSize: 20),
                    ),
                  );
                }).toList(),
              ),
            ),
          ],
        ),
      ),
    );
  }

  // ================================
  // 4. 输入弹框
  // ================================
  
  /// 文本输入弹框
  static Future<String?> showInput({
    required BuildContext context,
    required String title,
    String? placeholder,
    String? initialValue,
    String confirmText = '确定',
    String cancelText = '取消',
    int? maxLength,
    bool obscureText = false,
  }) {
    final controller = TextEditingController(text: initialValue);
    
    return showCupertinoDialog<String>(
      context: context,
      builder: (context) {
        return StatefulBuilder(
          builder: (context, setState) {
            return CupertinoAlertDialog(
              title: Text(title),
              content: SizedBox(
                height: 80,
                child: CupertinoTextField(
                  controller: controller,
                  placeholder: placeholder,
                  maxLength: maxLength,
                  obscureText: obscureText,
                  autofocus: true,
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 10,
                  ),
                  decoration: BoxDecoration(
                    border: Border.all(
                      color: CupertinoColors.quaternaryLabel,
                    ),
                    borderRadius: BorderRadius.circular(8),
                  ),
                ),
              ),
              actions: [
                CupertinoDialogAction(
                  child: Text(cancelText),
                  onPressed: () => Navigator.of(context).pop(),
                ),
                CupertinoDialogAction(
                  isDefaultAction: true,
                  child: Text(confirmText),
                  onPressed: () {
                    if (controller.text.isNotEmpty) {
                      Navigator.of(context).pop(controller.text);
                    }
                  },
                ),
              ],
            );
          },
        );
      },
    );
  }

  // ================================
  // 5. 加载弹框
  // ================================
  
  /// 显示加载提示
  static void showLoading({
    required BuildContext context,
    String message = '加载中...',
  }) {
    showCupertinoDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => CupertinoAlertDialog(
        content: SizedBox(
          height: 100,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const CupertinoActivityIndicator(radius: 16),
              const SizedBox(height: 16),
              Text(
                message,
                style: TextStyle(
                  fontSize: 14,
                  color: CupertinoColors.secondaryLabel.resolveFrom(context),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  /// 隐藏加载弹框
  static void hideLoading(BuildContext context) {
    Navigator.of(context).pop();
  }

  // ================================
  // 6. Toast 提示
  // ================================
  
  /// 显示 Toast 提示
  static void showToast({
    required BuildContext context,
    required String message,
    Duration duration = const Duration(seconds: 2),
    ToastPosition position = ToastPosition.center,
  }) {
    final overlay = Overlay.of(context);
    final overlayEntry = OverlayEntry(
      builder: (context) => _buildToastWidget(
        context: context,
        message: message,
        position: position,
      ),
    );

    overlay.insert(overlayEntry);
    Future.delayed(duration, overlayEntry.remove);
  }

  // ================================
  // 工具方法
  // ================================
  
  /// 构建选择器顶部工具栏
  static Widget _buildPickerToolbar({
    required BuildContext context,
    required String title,
    required String confirmText,
    required String cancelText,
    required VoidCallback onConfirm,
    required VoidCallback onCancel,
  }) {
    return Container(
      height: 44,
      decoration: BoxDecoration(
        color: CupertinoColors.secondarySystemBackground.resolveFrom(context),
        border: Border(
          bottom: BorderSide(
            color: CupertinoColors.separator.resolveFrom(context),
          ),
        ),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          CupertinoButton(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            onPressed: onCancel,
            child: Text(cancelText),
          ),
          Text(
            title,
            style: const TextStyle(
              fontSize: 17,
              fontWeight: FontWeight.w600,
            ),
          ),
          CupertinoButton(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            onPressed: onConfirm,
            child: Text(
              confirmText,
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
          ),
        ],
      ),
    );
  }

  /// 构建 Toast 组件
  static Widget _buildToastWidget({
    required BuildContext context,
    required String message,
    required ToastPosition position,
  }) {
    double? top, bottom;
    
    switch (position) {
      case ToastPosition.top:
        top = MediaQuery.of(context).padding.top + 50;
        break;
      case ToastPosition.center:
        top = MediaQuery.of(context).size.height / 2 - 25;
        break;
      case ToastPosition.bottom:
        bottom = MediaQuery.of(context).padding.bottom + 100;
        break;
    }

    return Positioned(
      top: top,
      bottom: bottom,
      left: 0,
      right: 0,
      child: Material(
        color: Colors.transparent,
        child: Center(
          child: Container(
            margin: const EdgeInsets.symmetric(horizontal: 20),
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
            decoration: BoxDecoration(
              color: CupertinoColors.systemFill.withOpacity(0.9),
              borderRadius: BorderRadius.circular(10),
            ),
            child: Text(
              message,
              style: const TextStyle(
                color: CupertinoColors.white,
                fontSize: 14,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

// ================================
// 数据结构
// ================================

/// ActionSheet 项目
class ActionSheetItem {
  final String title;
  final VoidCallback? onPressed;
  final bool destructive;

  ActionSheetItem({
    required this.title,
    this.onPressed,
    this.destructive = false,
  });
}

/// Toast 位置
enum ToastPosition {
  top,
  center,
  bottom,
}

💡 使用示例

基本使用方式

dart 复制代码
import 'ios_dialog.dart';

class ExamplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('弹框示例'),
      ),
      child: SafeArea(
        child: ListView(
          children: [
            // 1. 警告弹框
            CupertinoButton(
              child: const Text('基本警告'),
              onPressed: () => IosDialog.showAlert(
                context: context,
                title: '提示',
                message: '操作成功',
              ),
            ),

            // 2. 确认弹框
            CupertinoButton(
              child: const Text('确认弹框'),
              onPressed: () async {
                final result = await IosDialog.showConfirm(
                  context: context,
                  title: '确认删除',
                  message: '确定要删除吗?',
                  destructive: true,
                );
                if (result == true) {
                  IosDialog.showToast(
                    context: context,
                    message: '已删除',
                  );
                }
              },
            ),

            // 3. 底部菜单
            CupertinoButton(
              child: const Text('操作菜单'),
              onPressed: () {
                IosDialog.showSimpleActionSheet(
                  context: context,
                  title: '选择操作',
                  items: [
                    ActionSheetItem(
                      title: '编辑',
                      onPressed: () => print('编辑'),
                    ),
                    ActionSheetItem(
                      title: '删除',
                      destructive: true,
                      onPressed: () => print('删除'),
                    ),
                  ],
                );
              },
            ),

            // 4. 选择器
            CupertinoButton(
              child: const Text('选择器'),
              onPressed: () async {
                final result = await IosDialog.showPicker(
                  context: context,
                  items: ['选项一', '选项二', '选项三'],
                  itemBuilder: (item) => item,
                );
                if (result != null) {
                  print('选择了: $result');
                }
              },
            ),

            // 5. 输入弹框
            CupertinoButton(
              child: const Text('输入弹框'),
              onPressed: () async {
                final text = await IosDialog.showInput(
                  context: context,
                  title: '请输入',
                  placeholder: '输入内容...',
                );
                if (text != null) {
                  print('输入了: $text');
                }
              },
            ),

            // 6. 加载弹框
            CupertinoButton(
              child: const Text('加载弹框'),
              onPressed: () {
                IosDialog.showLoading(context: context);
                Future.delayed(const Duration(seconds: 2), () {
                  IosDialog.hideLoading(context);
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

🎨 设计特点

  1. 视觉一致性

所有组件都遵循 iOS 设计规范:

· 使用 Cupertino 组件库

· 圆角设计

· 系统配色

· 原生动画效果

  1. 用户体验优化

· 符合 iOS 操作习惯

· 流畅的过渡动画

· 合理的交互反馈

· 无障碍支持

  1. 开发者友好

· 类型安全的 API

· 完善的参数说明

· 清晰的错误提示

· 易于扩展和维护


📝 最佳实践

  1. 弹框层级管理
dart 复制代码
// 确保不会同时显示多个弹框
void showDialogSafely(BuildContext context) async {
  if (ModalRoute.of(context)?.isCurrent == true) {
    await IosDialog.showAlert(...);
  }
}
  1. 错误处理
dart 复制代码
void safeShowDialog(BuildContext context) {
  try {
    IosDialog.showAlert(...);
  } catch (e) {
    debugPrint('弹框显示失败: $e');
  }
}
  1. 内存管理
dart 复制代码
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  void dispose() {
    // 页面销毁时关闭所有弹框
    Navigator.of(context).popUntil((route) => route.isFirst);
    super.dispose();
  }
}

🔄 进阶用法

自定义扩展

dart 复制代码
/// 自定义带图标的弹框
extension IosDialogWithIcon on IosDialog {
  static Future<void> showAlertWithIcon({
    required BuildContext context,
    required String title,
    required IconData icon,
    Color iconColor = CupertinoColors.activeBlue,
  }) {
    return showCupertinoDialog(
      context: context,
      builder: (context) => CupertinoAlertDialog(
        title: Row(
          children: [
            Icon(icon, color: iconColor),
            const SizedBox(width: 8),
            Text(title),
          ],
        ),
        actions: [
          CupertinoDialogAction(
            child: const Text('确定'),
            onPressed: () => Navigator.pop(context),
          ),
        ],
      ),
    );
  }
}

📊 性能考虑

  1. 避免频繁弹框:控制弹框显示频率
  2. 及时释放资源:弹框关闭时清理资源
  3. 使用异步操作:避免阻塞 UI 线程
  4. 懒加载内容:复杂弹框内容延迟加载

🎯 总结

这个 iOS 弹框封装库提供了:

优点

· ✅ 完整的弹框类型覆盖

· ✅ 原生 iOS 视觉体验

· ✅ 简洁易用的 API

· ✅ 良好的性能表现

· ✅ 易于扩展和维护

适用场景

· iOS 专用应用开发

· 需要 iOS 风格体验的跨平台应用

· 对 UI/UX 一致性要求高的项目

后续优化方向

  1. 增加更多动画效果选项
  2. 支持暗色模式适配
  3. 添加国际化支持
  4. 提供更多自定义主题选项

通过这个封装,你可以快速为 Flutter 应用添加专业的 iOS 风格弹框,提升用户体验和开发效率。


相关推荐
LYFlied1 天前
浅谈跨端开发:大前端时代的融合之道
前端·flutter·react native·webview·大前端·跨端开发·hybrid
500841 天前
鸿蒙 Flutter 分布式数据同步:DistributedData 实时协同实战
分布式·flutter·华为·electron·开源·wpf·音视频
song5011 天前
鸿蒙 Flutter 图像编辑:原生图像处理与滤镜开发
图像处理·人工智能·分布式·flutter·华为·交互
●VON1 天前
从零构建可扩展 Flutter 应用:v1.0 → v2.0 全代码详解 -《已适配开源鸿蒙》
学习·flutter·开源·openharmony·开源鸿蒙
心随雨下1 天前
Flutter自适应布局部件(SafeArea 和 MediaQuery)总结
flutter·typescript
500841 天前
鸿蒙 Flutter 超级终端适配:多设备流转与状态无缝迁移
java·人工智能·flutter·华为·性能优化·wpf
帅气马战的账号11 天前
OpenHarmony 与 Flutter 深度集成:分布式能力与跨端 UI 实战进阶
flutter
小a彤1 天前
Flutter的核心优势
flutter
子春一1 天前
Flutter 与 AI 融合开发实战:集成大模型、智能图像识别与端侧推理,打造下一代智能应用
人工智能·flutter