Flutter 仿iOS Sheet Action 弹窗封装

前因

项目开发中总有一些常用的界面,需要能够重复利用,于是,有了这篇文章,期望效果如下:

需求分析

  1. 底部取消按钮,字体颜色单独显示
  2. 选项按钮并列显示,可能存在多个
  3. 选择后需要有对应的结果返回
  4. 整个弹窗背景色透明

根据以上需求,设计组件需要开放的属性如下:

arduino 复制代码
final String cancelTitle;//取消按钮标题
final String? title;//提示标题,此版本暂未实现显示标题
final List<String> actions;//选项按钮标题
final Color? cancelColor;//取消按钮颜色
final Color? titleColor;//标题颜色
final Color? actionColor;//选项标题颜色
final Color bgColor;//选项卡背景色
final String? fontFamily;//字体
final double fontSize;//字号
final double  selectionHeight;//每个选项的高度
final double borderRadius;//圆角大小
final FontWeight fontWeight;//字重
final ValueChanged<int>? selectAction;//选项结果回调

效果实现

拆分模块:

  1. 整体使用一个SizeBox包装,控制组件大小
  2. 整体展现形式为列表,先用一个Column包装
  3. 考虑到上层选项卡共用一个圆角和下层选项卡单独一个圆角,将使用两个Column将上下层拆分
  4. 圆角效果则用ClipRRect包裹
  5. 每个选项卡选中事件则直接用GestureDetector包装,返回点击事件
  6. 考虑到每个选项卡有背景色,所有用Container包裹一下,展示背景色
  7. 文字显示部分则直接使用Text这里用的SVText是对Text组件的封装,是为了便于开发和后期的修改,其本质就是一个Text,开放了一些常用属性。

上代码:

less 复制代码
Widget build(BuildContext context) {
  // TODO: implement build
  double width =  MediaQuery.of(context).size.width*0.9;//计算需要展示的宽度
  double safeBottomHeight = MediaQuery.of(context).padding.bottom;//底部需要留出安全区的位置
  double space = 10;//取消选项和其他选项的间距
  return SizedBox(//使用SizeBox进行包装
      width: width,//宽
      height: (selectionHeight + 1)*(actions.length + 1) + safeBottomHeight + space,//高
      child: Column(//分层
        children: [
          ClipRRect(//上层圆角
            borderRadius: BorderRadius.all(Radius.circular(borderRadius)),//圆角
            child: Column(//上层显示按钮
            //这里为了减少代码量,则直接将生成选项卡的代码逻辑封装了一下
              children: getActionSheet(context),
            ),
          ),
          Padding(//上层和取消选项间距
              padding: EdgeInsets.only(top: space),
          ),
          ClipRRect(//下层圆角
            borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
            //封装选项卡生成函数
            child: getSelectWidget(cancelTitle, true, -1,context),
          ),
          Padding(
              padding: EdgeInsets.only(top: safeBottomHeight),
          ),
        ],
      )
  );
}
//返回上层选项卡所有的视图
List<Widget> getActionSheet(BuildContext context){
  List<Widget>  actionsWidget = [];
  for (int i = 0;i < actions.length; i ++){
    String title = actions[i];
    //生成上层每个选项卡
    GestureDetector action = getSelectWidget(title, false, i, context);
    actionsWidget.add(action);
    if (i < actions.length - 1){
      Padding padding = const Padding(
          padding: EdgeInsets.only(top: 1)
      );
      actionsWidget.add(padding);
    }
  }
  return actionsWidget;
}
//选项卡生成函数
GestureDetector getSelectWidget(String title, bool isCancel, int tag, BuildContext context){
  return GestureDetector(//点击事件包裹
    child: Container(
      height: selectionHeight,
      width: double.infinity,
      color: bgColor,
      alignment: Alignment.center,
      child: SSLText(
        text: title,
        textColor: isCancel?cancelColor:actionColor,
        fontSize: fontSize,
        fontWeight: fontWeight,
        textDecoration: TextDecoration.none,
      ),
    ),
    onTap: (){
    //点击回调事件
      if (selectAction != null){
        selectAction!(tag);
      }
      //由于采用的是弹窗形式,则可使用pop获取返回值
      Navigator.of(context).pop(tag);
    },
  );
}

至此弹窗基本封装完毕。由于目前未涉及到使用标题的弹窗,标题功能暂未开发。

弹窗使用

弹窗封装完成后还需要有函数去展示这个弹窗,这里参考Flutter 对话框Flutter 弹窗样式。 最后选择使用showCupertinoModalPopup,这个弹窗的效果和iOS类似,只需将封装好的弹窗传递过去就好,剩下的动画效果和返回值系统已经处理好,直接用就行了。

上代码:

php 复制代码
static Future<int> showBottomSheet(BuildContext context,
    { required List<String> actions,
      required String cancelTitle,
      Color? titColor,
      Color? cancelColor,
      Color? actionColor,
      double? fontSize,
      String? fontFamily,
      double? selectionHeight,
      double? borderRadius,
      Color? bgColor,
      String? title,
      ValueChanged? selection,
    })async {
  //通过系统获取返回值,具体的可以研究下弹窗的原理,这个就是获取上面pop的返回值
  var result = await showCupertinoModalPopup(context: context, builder: (context){
    return SSLSheetAlert(//封装好的弹窗
      actions: actions,
      cancelTitle: cancelTitle,
      selectAction: selection,
      titleColor: titColor,
      cancelColor: cancelColor,
      actionColor: actionColor,
      fontSize: fontSize ?? 16,
      fontFamily: fontFamily,
      selectionHeight: selectionHeight??50,
      borderRadius: borderRadius??8,
      bgColor: bgColor??Colors.white,
      title: title,
    );
  });
  //需要额外处理的是,如果直接点击背景罩,此时返回的是null,所以这里需要特殊判断一下
  if (result == null){
    return -1;
  }
  return result;
}

最终用法:

php 复制代码
//获取系统返回结果
int result = await UIAlertTool.svShowBottomSheet(
    context,
    actions:["男", "女"],
    cancelTitle: "取消",
    actionColor: Colors.green,
    cancelColor: Colors.red,
    selection: (value){
    获取回调结果
      debugPrint("ssl get result 11 $value");
    }
);
debugPrint("ssl get result $result");

最终效果:

iOS

Android

注意:

在没有使用Material的时候,Text文本显示会和使用Material有区别,说下两个点:

  1. 没有使用的时候文本下面会有黄色的下划线,此时需要使用TextStyle中的decoration属性,设置为TextDecoration.none
  2. 字重在没有使用Material的时候会加粗,这个时候需要设置一下字重,通常设置为FontWeight.w500
相关推荐
恋猫de小郭10 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
明君8799715 小时前
Flutter 如何给图片添加多行文字水印
前端·flutter
ssshooter20 小时前
Tauri 踩坑 appLink 修改后闪退
前端·ios·rust
四眼肥鱼1 天前
flutter 利用flutter_libserialport 实现SQ800 串口通信
前端·flutter
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
火柴就是我2 天前
让我们实现一个更好看的内部阴影按钮
android·flutter
王晓枫2 天前
flutter接入三方库运行报错:Error running pod install
前端·flutter
开心就好20252 天前
UniApp开发应用多平台上架全流程:H5小程序iOS和Android
后端·ios
开心就好20252 天前
免 Xcode 的 iOS 开发新选择?聊聊一款更轻量的 iOS 开发 IDE kxapp 快蝎
后端·ios
恋猫de小郭2 天前
Apple 的 ANE 被挖掘,AI 硬件公开,宣传的 38 TOPS 居然是"数字游戏"?
前端·人工智能·ios