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
相关推荐
problc6 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
lqj_本人14 小时前
鸿蒙next选择 Flutter 开发跨平台应用的原因
flutter·华为·harmonyos
2401_8658548815 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
lqj_本人17 小时前
Flutter&鸿蒙next 状态管理框架对比分析
flutter·华为·harmonyos
起司锅仔21 小时前
Flutter启动流程(2)
flutter
hello world smile1 天前
最全的Flutter中pubspec.yaml及其yaml 语法的使用说明
android·前端·javascript·flutter·dart·yaml·pubspec.yaml
lqj_本人1 天前
Flutter 的 Widget 概述与常用 Widgets 与鸿蒙 Next 的对比
flutter·harmonyos
iFlyCai1 天前
极简实现酷炫动效:Flutter隐式动画指南第二篇之一些酷炫的隐式动画效果
flutter
lqj_本人1 天前
Flutter&鸿蒙next 中使用 MobX 进行状态管理
flutter·华为·harmonyos
lqj_本人1 天前
Flutter&鸿蒙next 中的 setState 使用场景与最佳实践
flutter·华为·harmonyos