为了应对复杂的业务场景,同时降低侵入性,在保持api稳定基础上,全面重构了SmartDialog底层
我现在可以自信的说:它现在是一个简洁,强大,侵入性极低的Pub包
请使用Flutter 2.0及其以上的小伙伴们移步:这一次,解决Flutter Dialog的各种痛点!
系统自带的Dialog实际上就是Push了一个新页面,这样存在很多好处,但是也存在一些很难解决的问题
上面这些痛点,简直个个致命
,当然,还存在一些其它的解决方案,例如:
很明显,使用Overlay可移植性最好,目前很多toast和dialog三方库便是使用该方案,使用了一些loading库,看了其中源码,穿透背景解决方案,和预期想要的效果大相径庭、一些dialog库自带toast显示,但是toast显示却又不能和dialog共存(toast属于特殊的信息展示,理应能独立存在),导致我需要多依赖一个Toast库
基于上面那些难以解决的问题,只能自己去实现,花了一些时间,实现了一个Pub包,基本该解决的痛点都已解决了,用于实际业务没什么问题
dependencies:
flutter_smart_dialog: any
# 非空安全前最后一个稳定版本
dependencies:
flutter_smart_dialog: ^1.3.1
void main() {
runApp(MyApp());
}
///flutter 2.0
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(),
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(child: child);
},
);
}
}
///flutter 1.x
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(),
builder: (BuildContext context, Widget child) {
return FlutterSmartDialog(child: child);
},
);
}
}
使用FlutterSmartDialog
包裹下child即可,下面就可以愉快的使用SmartDialog了
SmartDialog.showToast('test toast');
SmartDialog配置参数说明
即可//open loading
SmartDialog.showLoading();
//delay off
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();
Temp
为后缀的参数,和下述无Temp
为后缀的参数功能一致isUseExtraWidget
:是否使用额外覆盖浮层,可与主浮层独立开;可与loading,dialog之类独立开,自带的showToast便是开启了该配置,可与loading共存SmartDialog.show(
alignmentTemp: Alignment.bottomCenter,
clickBgDismissTemp: true,
widget: Container(
color: Colors.blue,
height: 300,
),
);
instance
里面暴露过多属性,导致使用不便,此处诸多参数使用instance
中的config
属性管理参数 | 功能说明 |
---|---|
alignment | 控制自定义控件位于屏幕的位置 Alignment.center: 自定义控件位于屏幕中间,且是动画默认为:渐隐和缩放,可使用isLoading选择动画 Alignment.bottomCenter、Alignment.bottomLeft、Alignment.bottomRight:自定义控件位于屏幕底部,动画默认为位移动画,自下而上,可使用animationDuration设置动画时间 Alignment.topCenter、Alignment.topLeft、Alignment.topRight:自定义控件位于屏幕顶部,动画默认为位移动画,自上而下,可使用animationDuration设置动画时间 Alignment.centerLeft:自定义控件位于屏幕左边,动画默认为位移动画,自左而右,可使用animationDuration设置动画时间 Alignment.centerRight:自定义控件位于屏幕左边,动画默认为位移动画,自右而左,可使用animationDuration设置动画时间 |
isPenetrate | 默认:false;是否穿透遮罩背景,交互遮罩之后控件,true:点击能穿透背景,false:不能穿透;穿透遮罩设置为true,背景遮罩会自动变成透明(必须) |
clickBgDismiss | 默认:true;点击遮罩,是否关闭dialog---true:点击遮罩关闭dialog,false:不关闭 |
maskColor | 遮罩颜色(isPenetrate为true,该参数失效) |
maskWidget | 可高度自定义遮罩样式,使用该参数,maskColor失效(isPenetrate为true,该参数失效) |
animationDuration | 动画时间 |
isUseAnimation | 默认:true;是否使用动画 |
isLoading | 默认:true;是否使用Loading动画;true:内容体使用渐隐动画 false:内容体使用缩放动画,仅仅针对中间位置的dialog |
isExist | 状态标定:loading和自定义dialog 是否存在在界面上 |
isExistMain | 状态标定:自定义dialog 是否存在在界面上(show) |
isExistLoading | 状态标定:loading是否存在界面上(showLoading) |
isExistToast | 状态标定:toast是否存在在界面上(showToast) |
SmartDialog.instance.config
..clickBgDismiss = true
..isLoading = true
..isUseAnimation = true
..animationDuration = Duration(milliseconds: 270)
..isPenetrate = false
..maskColor = Colors.black.withOpacity(0.1)
..alignment = Alignment.center;
使用Overlay的依赖库,基本都存在一个问题,难以对返回事件的监听,导致触犯返回事件难以关闭弹窗布局之类,想了很多办法,没办法在依赖库中解决该问题,此处提供一个BaseScaffold
,在每个页面使用BaseScaffold
,便能解决返回事件关闭Dialog问题
typedef ScaffoldParamVoidCallback = void Function();
class BaseScaffold extends StatefulWidget {
const BaseScaffold({
Key? key,
this.appBar,
this.body,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer,
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomInset,
this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.drawerScrimColor,
this.drawerEdgeDragWidth,
this.drawerEnableOpenDragGesture = true,
this.endDrawerEnableOpenDragGesture = true,
this.isTwiceBack = false,
this.isCanBack = true,
this.onBack,
}) : super(key: key);
final bool extendBody;
final bool extendBodyBehindAppBar;
final PreferredSizeWidget? appBar;
final Widget? body;
final Widget? floatingActionButton;
final FloatingActionButtonLocation? floatingActionButtonLocation;
final FloatingActionButtonAnimator? floatingActionButtonAnimator;
final List<Widget>? persistentFooterButtons;
final Widget? drawer;
final Widget? endDrawer;
final Color? drawerScrimColor;
final Color? backgroundColor;
final Widget? bottomNavigationBar;
final Widget? bottomSheet;
final bool? resizeToAvoidBottomInset;
final bool primary;
final DragStartBehavior drawerDragStartBehavior;
final double? drawerEdgeDragWidth;
final bool drawerEnableOpenDragGesture;
final bool endDrawerEnableOpenDragGesture;
//custom param
final bool isTwiceBack;
final bool isCanBack;
final ScaffoldParamVoidCallback? onBack;
@override
_BaseScaffoldState createState() => _BaseScaffoldState();
}
class _BaseScaffoldState extends State<BaseScaffold> {
DateTime? _lastTime;
@override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
appBar: widget.appBar,
body: widget.body,
floatingActionButton: widget.floatingActionButton,
floatingActionButtonLocation: widget.floatingActionButtonLocation,
floatingActionButtonAnimator: widget.floatingActionButtonAnimator,
persistentFooterButtons: widget.persistentFooterButtons,
drawer: widget.drawer,
endDrawer: widget.endDrawer,
bottomNavigationBar: widget.bottomNavigationBar,
bottomSheet: widget.bottomSheet,
backgroundColor: widget.backgroundColor,
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
primary: widget.primary,
drawerDragStartBehavior: widget.drawerDragStartBehavior,
extendBody: widget.extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
drawerScrimColor: widget.drawerScrimColor,
drawerEdgeDragWidth: widget.drawerEdgeDragWidth,
drawerEnableOpenDragGesture: widget.drawerEnableOpenDragGesture,
endDrawerEnableOpenDragGesture: widget.endDrawerEnableOpenDragGesture,
),
onWillPop: _dealWillPop,
);
}
Future<bool> _dealWillPop() async {
widget.onBack?.call();
if (SmartDialog.instance.config.isExist) {
SmartDialog.dismiss();
return false;
}
if (!widget.isCanBack) {
return false;
}
var now = DateTime.now();
var condition =
_lastTime == null || now.difference(_lastTime!) > Duration(seconds: 1);
if (widget.isTwiceBack && condition) {
_lastTime = now;
SmartDialog.showToast("再点一次退出");
return false;
}
return true;
}
}
typedef ScaffoldParamVoidCallback = void Function();
class BaseScaffold extends StatefulWidget {
const BaseScaffold({
Key key,
this.appBar,
this.body,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer,
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomInset,
this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.drawerScrimColor,
this.drawerEdgeDragWidth,
this.drawerEnableOpenDragGesture = true,
this.endDrawerEnableOpenDragGesture = true,
this.isTwiceBack = false,
this.isCanBack = true,
this.onBack,
}) : assert(primary != null),
assert(extendBody != null),
assert(extendBodyBehindAppBar != null),
assert(drawerDragStartBehavior != null),
super(key: key);
///系统Scaffold的属性
final bool extendBody;
final bool extendBodyBehindAppBar;
final PreferredSizeWidget appBar;
final Widget body;
final Widget floatingActionButton;
final FloatingActionButtonLocation floatingActionButtonLocation;
final FloatingActionButtonAnimator floatingActionButtonAnimator;
final List<Widget> persistentFooterButtons;
final Widget drawer;
final Widget endDrawer;
final Color drawerScrimColor;
final Color backgroundColor;
final Widget bottomNavigationBar;
final Widget bottomSheet;
final bool resizeToAvoidBottomInset;
final bool primary;
final DragStartBehavior drawerDragStartBehavior;
final double drawerEdgeDragWidth;
final bool drawerEnableOpenDragGesture;
final bool endDrawerEnableOpenDragGesture;
///增加的属性
///点击返回按钮提示是否退出页面,快速点击俩次才会退出页面
final bool isTwiceBack;
///是否可以返回
final bool isCanBack;
///监听返回事件
final ScaffoldParamVoidCallback onBack;
@override
_BaseScaffoldState createState() => _BaseScaffoldState();
}
class _BaseScaffoldState extends State<BaseScaffold> {
DateTime _lastPressedAt; //上次点击时间
@override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
appBar: widget.appBar,
body: widget.body,
floatingActionButton: widget.floatingActionButton,
floatingActionButtonLocation: widget.floatingActionButtonLocation,
floatingActionButtonAnimator: widget.floatingActionButtonAnimator,
persistentFooterButtons: widget.persistentFooterButtons,
drawer: widget.drawer,
endDrawer: widget.endDrawer,
bottomNavigationBar: widget.bottomNavigationBar,
bottomSheet: widget.bottomSheet,
backgroundColor: widget.backgroundColor,
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
primary: widget.primary,
drawerDragStartBehavior: widget.drawerDragStartBehavior,
extendBody: widget.extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
drawerScrimColor: widget.drawerScrimColor,
drawerEdgeDragWidth: widget.drawerEdgeDragWidth,
drawerEnableOpenDragGesture: widget.drawerEnableOpenDragGesture,
endDrawerEnableOpenDragGesture: widget.endDrawerEnableOpenDragGesture,
),
onWillPop: dealWillPop,
);
}
///控件返回按钮
Future<bool> dealWillPop() async {
if (widget.onBack != null) {
widget.onBack();
}
//处理弹窗问题
if (SmartDialog.instance.config.isExist) {
SmartDialog.dismiss();
return false;
}
//如果不能返回,后面的逻辑就不走了
if (!widget.isCanBack) {
return false;
}
if (widget.isTwiceBack) {
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
//两次点击间隔超过1秒则重新计时
_lastPressedAt = DateTime.now();
//弹窗提示
SmartDialog.showToast("再点一次退出");
return false;
}
return true;
} else {
return true;
}
}
}
当时想解决穿透暗色背景,和背景后面的控件互动的时候,我几乎立马想到这俩个控件,先了解下这俩个控件吧
AbsorbPointer
本身可以响应事件,消耗掉事件absorbing
属性(默认true) AbsorbPointer(
absorbing: true,
child: Listener(
onPointerDown: (event){
print('+++++++++++++++++++++++++++++++++');
},
)
)
IgnorePointer
本身无法响应事件,其下的控件可以接收到点击事件(父控件)ignoring
属性(默认true) IgnorePointer(
ignoring: true,
child: Listener(
onPointerDown: (event){
print('----------------------------------');
},
)
)
分析
AbsorbPointer
这个控件是不合适的,因为AbsorbPointer
本身会消费触摸事件,事件被AbsorbPointer
消费掉,会导致背景后的页面无法获取到触摸事件;IgnorePointer
本身无法消费触摸事件,又由于IgnorePointer
和AbsorbPointer
都具有屏蔽子Widget获取触摸事件的作用,这个貌似靠谱,这里试了,可以和背景后面的页面互动!但是又存在一个十分坑的问题IgnorePointer
屏蔽子控件的触摸事件,而IgnorePointer
本身又不消耗触摸事件,会导致无法获取到背景的点击事件!这样点击背景会无法关闭dialog弹窗,只能手动关闭dialog;各种尝试,实在没办法获取到背景的触摸事件,此种穿透背景的方案只能放弃这种方案,成功实现想要的穿透效果,这里了解下behavior
的几种属性
有戏了!很明显translucent是有希望的,尝试了几次,然后成功实现了想要的效果
注意,这边有几个坑点,提一下
Listener
控件来使用behavior属性,使用GestureDetector中behavior属性会存在一个问题,一般来说:都是Stack控件里面的Children,里面有俩个控件,分上下层,在此处,GestureDetector设置behavior属性,俩个GestureDetector控件上下叠加,会导致下层GestureDetector获取不到触摸事件,很奇怪;使用Listener
不会产生此问题Container
控件,我这里设置了Colors.transparent
,直接会导致下层接受不到触摸事件,color为空才能使下层控件接受到触摸事件,此处不要设置color即可下面是写的一个验证小示例
class TestLayoutPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _buildBg(children: [
//下层
Listener(
onPointerDown: (event) {
print('下层蓝色区域++++++++');
},
child: Container(
height: 300,
width: 300,
color: Colors.blue,
),
),
//上层 事件穿透
Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {
print('上层区域---------');
},
child: Container(
height: 200,
width: 200,
),
),
]);
}
Widget _buildBg({List<Widget> children}) {
return Scaffold(
appBar: AppBar(title: Text('测试布局')),
body: Center(
child: Stack(
alignment: Alignment.center,
children: children,
),
),
);
}
}
OverlayEntry
和OverlayEntryExtra
可以高度自定义,相关实现,可查看内部实现show()
方法中的isUseExtraWidget
区分这个库花了一些时间去构思和实现,算是解决几个很大的痛点
返回事件
有什么好的处理思路,麻烦在评论里告知,谢谢!FlutterSmartDialog一些信息
状态管理
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。