flutter-编写ios风格dialog、picker
前言
flutter
开发过程中,除了加载框之类的,dialog
系列也会头疼,包括alert弹窗提示
、picker
、date-picker
等也需要自行选择或定制,很麻烦,且不一定能使用到自己想要的效果
这里自己更偏好ios风格
,因此使用 Cupertino
风格对应组件封装成了我们直接可以用的弹窗、picker
系列组件,且默认的效果也不是很好,因此自行改动了一下
本来是想使用 adaptive_dialog 弹窗,但感觉两个平台两种风格的差点意思,个人更偏好 ios风格,于是乎才想起来单独做了 ios 风格的弹窗
然后参考了 flutter-widget ,发现 picker 用起来也不是那么给力,就也重新定制了一下,顺道增加了比较常用风格的 picker
、mutli-picker
、date-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
,效果如下所示
使用如下所示,返回的结果在 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
,效果如下所示
使用如下所示,返回 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
,效果如下所示
使用如下所示,返回 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
,效果如下所示
使用如下所示,需要传入一维数组,默认索引不传递则为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
,根据需要使用,效果如下所示
跟 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
,也间接提高了效率
使用效果如下所示,结果返回:数据源(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(用的不多也介绍一下)
看一下效果图
使用如下所示,外面需要放一个 Container
或 SizeBox
控制外部点击的按钮大小,显示效果就如上所示,由于使用比较少,且功能比较简单,定制多余,就不封装了
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