likes
comments
collection
share

flutter-编写ios风格dialog、picker

作者站长头像
站长
· 阅读数 12

前言

flutter 开发过程中,除了加载框之类的,dialog系列也会头疼,包括alert弹窗提示pickerdate-picker等也需要自行选择或定制,很麻烦,且不一定能使用到自己想要的效果

这里自己更偏好ios风格,因此使用 Cupertino风格对应组件封装成了我们直接可以用的弹窗、picker系列组件,且默认的效果也不是很好,因此自行改动了一下

本来是想使用 adaptive_dialog 弹窗,但感觉两个平台两种风格的差点意思,个人更偏好 ios风格,于是乎才想起来单独做了 ios 风格的弹窗

然后参考了 flutter-widget ,发现 picker 用起来也不是那么给力,就也重新定制了一下,顺道增加了比较常用风格的 pickermutli-pickerdate-picker以便于使用

案例demo(alert、action-sheet、input-alert、picker、mutli-picker、date-picker)

如果想发布到 pub.dev 可以参考这里

本章也增加了如何避免使用 dialog 时传递 context 的方法

ps:除此之外还增加了 ios 风格的 menu 案例,仅作为参考,未封装成组件

ps2:如果该库和其他的名字有冲突,可以使用模块化方式导入,liru: import 'dialogs/dialogs.dart' as dialog;

alert

ios 风格的 alert,效果如下所示

flutter-编写ios风格dialog、picker

使用如下所示,返回的结果在 Future 中,为bool类型,毕竟取消位置也可能不是取消而是有其他事件

showCupertinoAlert(
  // context: context,
  title: "提示",
  message: "检测到未登录,请前往登陆!",
  confirmText: "立即前往",
  cancelText: "取消",
  // isDestructiveCancel: true,
).then((res) {
  print(res);
  setState(() {
    value = res.toString();
  });
});

一共提供了这些属性,可以根据情况传递使用

BuildContext? context, //如果没设置全局,需要传递自己的context
String title = '',
String message = '',
confirmText = '确定',
cancelText = '取消',
isShowCancel = true,
isDestructiveConfirm = false,
isDestructiveCancel = false,

input-alert

ios 风格的 input-alert,效果如下所示

flutter-编写ios风格dialog、picker

使用如下所示,返回 Future 为输入的 text,取消则不触发回调

showCupertinoInputAlert(
  // context: context,
  title: "标题",
  message: '内容',
  isDestructiveCancel: true,
  onChanged: (String text) {
    print(text);
    setState(() {
      value = text;
    });
  },
).then((res) {
  print(res);
  //可以干别的事情
  if (res != null) {
    setState(() {
      value = res;
    });
  }
});

具体参数如下所示,通过名字就知道功能了,不多介绍

BuildContext? context, //如果没设置全局,需要传递自己的context
String title = '',
String message = '',
confirmText = '确定',
cancelText = '取消',
isShowCancel = true,
isDestructiveConfirm = false,
isDestructiveCancel = false,
String? placeholder,
String? defaultText,
TextAlign? textAlign,
OverlayVisibilityMode? clearButtonMode,
TextInputType? keyboardType,
String? Function(String value)? onChanged, //内容改变后的回调,可以通过返回值来校验text输入等操作

action-sheet

ios 风格的 action-sheet,效果如下所示

flutter-编写ios风格dialog、picker

使用如下所示,返回 Future 为选择的 index,内容可以根据 外部的actions 获取实际选择内容,取消不触发回调

可以通过 isDestructive、isLoading 可选参数,来为单独的按钮置红或者显示加载中,如果有未加载完毕的可以使用 isLoading 参数来显示加载中的情况

final actions = <ActionSheetItem>[
  ActionSheetItem(text: "确定"),
  ActionSheetItem(text: "特殊", isDestructive: true),
  ActionSheetItem(text: "加载中...", isLoading: true),
];
showCupertinoActionSheet(
  // context: context,
  title: '演示',
  message: "请选择一个效果",
  actions: actions, //使用了updater之后,该参数可以忽略
  // isDestructiveCancel: true,
).then((res) {
  print(res);
  setState(() {
    value = actions[res].text!;
  });
});

如果用到了网络数,需要用到加载中,可以使用

final ActionSheetUpdater updater = ActionSheetUpdater();

getActionSheetData() {
  //有网络数据时请求建议使用该参数
  //给定默认效果
  updater.actions = <ActionSheetItem>[
    // ActionSheetItem(text: "确定"),
    // ActionSheetItem(text: "特殊", isDestructive: true),
    ActionSheetItem(text: "加载中...", isLoading: true),
  ];
  Future.delayed(const Duration(seconds: 5), () {
    setState(() {
      value = '加载完了';
    });
    //如果要用到前面的actions,可以从updater中获取
    // final actions = updater.actions;
    //也可以先赋值action后调用更新,也可以像下面似的直接传参更新
    updater.update(<ActionSheetItem>[
      ActionSheetItem(text: "确定"),
      ActionSheetItem(text: "特殊", isDestructive: true),
      ActionSheetItem(text: "加载中...", isLoading: true),
    ]);
  });
}

void showActionSheet() {
  //假设这是网络数据
  showCupertinoActionSheet(
    // context: context,
    title: '演示',
    message: "请选择一个效果",
    // actions: actions,//使用了updater之后,该参数可以忽略
    updater: updater,
  ).then((res) {
    print(res);
    setState(() {
      value = actions[res].text!;
    });
  });
}

参数如下所示

BuildContext? context, //如果没设置全局,需要传递自己的context
String? title,
String? message,
ActionSheetUpdater? updater, //外部声明该变量用与实时更新action
List<ActionSheetItem>? actions,
isShowCancel = true,
cancelText = '取消',
isDefaultActionCancel = false, //粗体
isDestructiveCancel = false, //红色

picker

单列picker,效果如下所示

flutter-编写ios风格dialog、picker

使用如下所示,需要传入一维数组,默认索引不传递则为0,结果返回索引index选择的item

showCupertinoPicker(
  // context: context,
  pickerList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
  defaultIndex: 2,
  onValueChanged: (index, item) {
    //picker滑动的回调
    print(index);
    print(item);
  },
).then((res) {
  setState(() {
    value = res.item.toString();
  });
  print(res.index);
  print(res.item);
});

参数如下所示

BuildContext? context, //如果没设置全局,需要传递自己的context
required List pickerList,
defaultIndex = 0,
String? title = '',
String? confirmText = '确定',
String? cancelText = '取消',
Color? confirmColor,
Color? cancelColor,
void Function(int index, dynamic item)? onValueChanged,

mutli-picker

多列picker,根据需要使用,效果如下所示

flutter-编写ios风格dialog、picker

picker 类似,只不过传递的数据源为多维数组(建议不超过6组),结果返回:数据源(pickerList)、索引(indexs)、选择的列表(selectList)

showCupertinoMutliPicker(
  // context: context,
  pickerList: [
    [100, 200, 300, 400, 500],
    [10, 20, 30, 40, 50],
    [1, 2, 3, 4, 5],
  ],
  onValueChanged: (indexs) {
    print(indexs);
  },
).then((res) {
  setState(() {
    value = res.selectList.join(',');
  });
  print(res);
});

参数如下所示,跟 picker 类似

BuildContext? context, //如果没设置全局,需要传递自己的context
required List<List> pickerList,
List<int>? defaultIndex,
List<String>? units, //单位,末尾是否使用单位,传入就显示
String? title = '',
String? confirmText = '确定',
String? cancelText = '取消',
Color? confirmColor,
Color? cancelColor,
double? itemFontSize,
void Function(List<int> index)? onValueChanged, //由于pickerList外面传递进来的,因此里面不在返回其他

date-picker

时间选择器 date-picker,一个选择日期时间的选择器,可以设置日期区间,由于是直接展示弹窗的方式,不便于修改,直接使用新的 mutli-picker 编写的,而不是基于前面的 mutli-picker,也间接提高了效率

flutter-编写ios风格dialog、picker

使用效果如下所示,结果返回:数据源(pickerList)、索引(indexs)、选择的列表(selectList),拼接的日期字符串(dateString)

void showIosStyleDatePicker(DatePickerType pickerMode) {
  showCupertinoDatePicker(
    // context: context,
    pickerMode: pickerMode,
    beforeYearsInterval: 10,
    afterYearsInterval: 10,
    // minDate: DateTime.now().subtract(const Duration(days: 1000)),
    // maxDate: DateTime.now().add(const Duration(days: 1000)),
  ).then((res) {
    print(res);
    setState(() {
      value = res.dateString;
    });
  });
}

使用如下所示,其中 units 为单位,时间间隔看下面参数即可,都是成双成对出现,要不都不传递使用默认

BuildContext? context, //如果没设置全局,需要传递自己的context
DatePickerType pickerMode = DatePickerType.dateTimeMinute,
List<int>? defaultIndexs,
List<String>? units = const [], //如果想使用自己的单位正常传递,如果不显示默认单位传递null即可
DateTime? dateTime, //传入时间,可以控制默认选择位置,默认当前时间
DateTime? minDate, //可以设置前后截止时间,优先该属性
DateTime? maxDate,
int? beforeYearsInterval, //根据当前时间或者传入时间向前向后多少年,默认前后20年,以一年365天为基准
int? afterYearsInterval,
String title = '',
confirmText = '确定',
cancelText = '取消',
double? itemFontSize,
Color? confirmColor,
Color? cancelColor,

menu(用的不多也介绍一下)

看一下效果图

flutter-编写ios风格dialog、picker

使用如下所示,外面需要放一个 ContainerSizeBox 控制外部点击的按钮大小,显示效果就如上所示,由于使用比较少,且功能比较简单,定制多余,就不封装了

CupertinoContextMenu(
  actions: <Widget>[
    CupertinoContextMenuAction(
      onPressed: () {
        Navigator.pop(context);
      },
      isDefaultAction: true,
      trailingIcon: CupertinoIcons.doc_on_clipboard_fill,
      child: const Text('Copy'),
    ),
    CupertinoContextMenuAction(
      onPressed: () {
        Navigator.pop(context);
      },
      trailingIcon: CupertinoIcons.share,
      child: const Text('Share'),
    ),
    CupertinoContextMenuAction(
      onPressed: () {
        Navigator.pop(context);
      },
      trailingIcon: CupertinoIcons.heart,
      child: const Text('Favorite'),
    ),
    CupertinoContextMenuAction(
      onPressed: () {
        Navigator.pop(context);
      },
      isDestructiveAction: true,
      trailingIcon: CupertinoIcons.delete,
      child: const Text('Delete'),
    ),
  ],
  child: Container(
    decoration: BoxDecoration(
      color: CupertinoColors.systemYellow,
      borderRadius: BorderRadius.circular(20.0),
    ),
    child: const FlutterLogo(size: 500.0),
  ),

不传递context方式弹出dialog

案例使用的是方案一,预留该类,方便后续添加设置属性(案例中并没有扩展其他属性,仅仅预留使用)

方案一 InheritedWidget + navigatorKey

我们使用 InheritedWidget + navigatorKey来解决即可

InheritedWidget 设置,为了方便使用,使用了静态变量

class DialogConfig extends InheritedWidget {
  static GlobalKey<NavigatorState>? globalNavigatorKey;

  const DialogConfig.internal({
    Key? key,
    required Widget child,
  }) : super(key: key, child: child);

  factory DialogConfig({
    Key? key,
    required GlobalKey<NavigatorState> globalNavigatorKey,
    required Widget child,
  }) {
    DialogConfig.globalNavigatorKey = globalNavigatorKey;
    return DialogConfig.internal(
      key: key,
      child: child,
    );
  }

  /*
    GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

    DialogConfig(
      globalNavigatorKey: navigatorKey,
      child: MaterialApp(
        navigatorKey: navigatorKey,
        ...,
      ),
    );
   */
  static BuildContext get context {
    assert(globalNavigatorKey?.currentState?.context != null, '获取globalcontext失败,请在main函数中的 MaterialApp 外层,设置 DialogConfig,且传入MaterialApp的navigatorKey');
    return globalNavigatorKey!.currentState!.context;
  }

  //就像 Navigator.of(context)一样获取,用于调用内部参数
  static DialogConfig of(BuildContext context) {
    final DialogConfig? result = context.dependOnInheritedWidgetOfExactType<DialogConfig>();
    assert(result != null, 'No DialogConfig found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(DialogConfig oldWidget) {
    return false;
  }
}

MaterialApp 所在类

GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

DialogConfig(
  globalNavigatorKey: navigatorKey,
  child: MaterialApp(
    navigatorKey: navigatorKey,
    debugShowCheckedModeBanner: false,
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: const MyHomePage(),
  ),
);

方案二 单例类 + navigatorKey(简单粗暴)

创建一个保存单例类, 然后 MaterialApp 所在出往单例类传入navigatorKey参数即可

class DialigConfig {
  static GlobalKey<NavigatorState>? globalNavigatorKey;

  static BuildContext get context {
    assert(globalNavigatorKey?.currentState?.context != null, '获取globalcontext失败,请在 MaterialApp 所在处,设置 DialogConfig,且传入MaterialApp的navigatorKey');
    return globalNavigatorKey!.currentState!.context;
  }
}

然后直接传入即可

DialigConfig1.globalNavigatorKey = navigatorKey;

最后

可以直接拉进自己的代码仓库使用,快来试试吧

转载自:https://juejin.cn/post/7175154405501567036
评论
请登录