一种更优雅的Flutter Dialog解决方案
前言
系统自带的Dialog实际上就是Push了一个新页面,这样存在很多好处,但是也存在一些很难解决的问题
- 必须传BuildContext
- loading弹窗一般都封装在网络框架中,多传个context参数就很头疼;用fish_redux还好,effect层直接能拿到context,要是用bloc还得在view层把context传到bloc或者cubit里面。。。
- 无法穿透暗色背景,点击dialog后面的控件
- 这个是真头痛,想了很多办法都没在自带dialog上面解决
- 系统自带Dialog写成的Loading弹窗,在网络请求和跳转页面的情况,会存在路由混乱的情况
- 情景复盘:loading库封装在网络层,某个页面提交完表单,要跳转页面,提交操作完成,进行页面跳转,loading关闭是在异步回调中进行(onError或者onSuccess),会出现执行了跳转操作时,弹窗还未关闭,延时一小会关闭,因为用的都是pop页面方法,会把跳转的页面pop掉
- 上面是一种很常见的场景,涉及到复杂场景更加难以预测,解决方法也有:定位页面栈的栈顶是否是Loading弹窗,选择性Pop,实现麻烦
上面这些痛点,简直个个致命,当然,还存在一些其它的解决方案,例如:
- 每个页面顶级使用Stack
- 使用Overlay
很明显,使用Overlay可移植性最好,目前很多Toast和dialog三方库便是使用该方案,使用了一些loading库,看了其中源码,穿透背景解决方案,和预期想要的效果大相径庭、一些dialog库自带toast显示,但是toast显示却又不能和dialog共存(toast属于特殊的信息展示,理应能独立存在),导致我需要多依赖一个Toast库
SmartDialog
基于上面那些难以解决的问题,只能自己去实现,花了一些时间,实现了一个Pub包,基本该解决的痛点都已解决了,用于实际业务没什么问题
效果

引入
dependencies:
flutter_smart_dialog: ^1.0.1
使用
- 主入口配置
- 在主入口这地方需要配置,这样就可以不传BuildContext使用Dialog
- 只需要在MaterialApp的builder参数处配置下即可
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: SmartDialogPage(),
builder: (BuildContext context, Widget child) {
return Material(
type: MaterialType.transparency,
child: FlutterSmartDialog(child: child),
);
},
);
}
}
使用FlutterSmartDialog包裹下child即可,下面就可以愉快的使用SmartDialog了
- 使用Toast
- msg:必传信息
- time:可选,Duration类型
- alignment:可控制toast位置
- 如果想使用花里花哨的Toast效果,使用show方法定制就行了,炒鸡简单喔,懒得写,抄下我的ToastWidget,改下属性即可
SmartDialog.instance.showToast('test toast');
- 使用Loading
//open loading
SmartDialog.instance.showLoading();
//delay off
await Future.delayed(Duration(seconds: 2));
SmartDialog.instance.dismiss();
- 自定义dialog
- 使用SmartDialog.instance.show()方法即可,里面含有众多'Temp'为后缀的参数,和下述无'Temp'为后缀的参数功能一致
SmartDialog.instance.show(
alignmentTemp: Alignment.bottomCenter,
clickBgDismissTemp: true,
widget: Container(
color: Colors.blue,
height: 300,
),
);
- SmartDialog配置参数说明
- 为了避免
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 | 默认:false;点击遮罩,是否关闭dialog---true:点击遮罩关闭dialog,false:不关闭 |
| maskColor | 遮罩颜色 |
| animationDuration | 动画时间 |
| isUseAnimation | 默认:true;是否使用动画 |
| isLoading | 默认:true;是否使用Loading动画;true:内容体使用渐隐动画 false:内容体使用缩放动画,仅仅针对中间位置的控件 |
| isExist | 默认:false;主体SmartDialog(OverlayEntry)是否存在在界面上 |
| isExistExtra | 默认:false;额外SmartDialog(OverlayEntry)是否存在在界面上 |
- 返回事件,关闭弹窗解决方案
使用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.resizeToAvoidBottomPadding,
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 resizeToAvoidBottomPadding;
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,
resizeToAvoidBottomPadding: widget.resizeToAvoidBottomPadding,
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.instance.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.instance.showToast("再点一次退出");
return false;
}
return true;
} else {
return true;
}
}
}
几个问题解决方案
穿透背景
- 穿透背景有俩个解决方案,这里都说明下
AbsorbPointer、IgnorePointer
当时想解决穿透暗色背景,和背景后面的控件互动的时候,我几乎立马想到这俩个控件,先了解下这俩个控件吧
- AbsorbPointer
阻止子树接收指针事件,
AbsorbPointer本身可以响应事件,消耗掉事件absorbing属性(默认true)- true:拦截向子Widget传递的事件 false:不拦截
AbsorbPointer(
absorbing: true,
child: Listener(
onPointerDown: (event){
LogUtil.log('+++++++++++++++++++++++++++++++++');
},
)
)
- IgnorePointer
- 阻止子树接收指针事件,
IgnorePointer本身无法响应事件,其下的控件可以接收到点击事件(父控件) ignoring属性(默认true)- true:拦截向子Widget传递的事件 false:不拦截
- 阻止子树接收指针事件,
IgnorePointer(
ignoring: true,
child: Listener(
onPointerDown: (event){
LogUtil.log('----------------------------------');
},
)
)
分析
- 这里来分析下,首先
AbsorbPointer这个控件是不合适的,因为AbsorbPointer本身会消费触摸事件,事件被AbsorbPointer消费掉,会导致背景后的页面无法获取到触摸事件;IgnorePointer本身无法消费触摸事件,又由于IgnorePointer和AbsorbPointer都具有屏蔽子Widget获取触摸事件的作用,这个貌似靠谱,这里试了,可以和背景后面的页面互动!但是又存在一个十分坑的问题 - 因为使用
IgnorePointer屏蔽子控件的触摸事件,而IgnorePointer本身又不消耗触摸事件,会导致无法获取到背景的点击事件!这样点击背景会无法关闭dialog弹窗,只能手动关闭dialog;各种尝试,实在没办法获取到背景的触摸事件,此种穿透背景的方案只能放弃
Listener、behavior
这种方案,成功实现想要的穿透效果,这里了解下behavior的几种属性
- deferToChild:仅当一个孩子被命中测试击中时,屈服于其孩子的目标才会在其范围内接收事件
- opaque:不透明目标可能会受到命中测试的打击,导致它们既在其范围内接收事件,又在视觉上阻止位于其后方的目标也接收事件
- translucent:半透明目标既可以接收其范围内的事件,也可以在视觉上允许目标后面的目标也接收事件
有戏了!很明显translucent是有希望的,尝试了几次,然后成功实现了想要的效果
注意,这边有几个坑点,提一下
务必使用
Listener控件来使用behavior属性,使用GestureDetector中behavior属性会存在一个问题,一般来说:都是Stack控件里面的Children,里面有俩个控件,分上下层,在此处,GestureDetector设置behavior属性,俩个GestureDetector控件上下叠加,会导致下层GestureDetector获取不到触摸事件,很奇怪;使用Listener不会产生此问题我们的背景使用
Container控件,里面的color不要设置值,我这里设置了Colors.transparent,直接会导致下层接受不到触摸事件,color为空才能使下层控件接受到触摸事件,此处不要设置color即可
下面是写的一个验证小示例
class TestLayoutPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _buildBg(children: [
//底下
Listener(
onPointerDown: (event) {
print(context, '底部蓝色区域++++++++');
},
child: Container(
height: 300,
width: 300,
color: Colors.blue,
),
),
//上面 事件穿透
Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {
print(context, '上面红色区域---------');
},
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,
),
),
);
}
}
Toast和Loading冲突
这个问题,从理论上肯定会存在的,因为一般Overlay库只会使用一个OverlayEntry控件,这会导致,全局只能存在一个浮窗布局,Toast本质是一个全局弹窗,Loading也是一个全局弹窗,使用其中一个都会导致另一个消失
Toast明显是应该独立于其他弹窗的一个消息提示,封装在网络库中的关闭弹窗的dismiss方法,也会将Toast消息在不适宜的时间关闭,在实际开发中就碰到此问题,只能多引用一个Toast三方库来解决,在规划这个dialog库的时候,就像必须解决此问题
- 此处内部多使用了一个OverlayEntry来解决该问题,提供了相关参数来分别控制,完美使Toast独立于其它的dialog弹窗
- 此处只多提供一个OverlayEntryExtra,如果需要更多,可copy本库,自行定义,多增加一个OverlayEntry都会让内部逻辑和方法使用急剧复杂,维护也会变得不可预期,故只多提供一个OverlayEntry
FlutterSmartDialog提供
OverlayEntry和OverlayEntryExtra可以高度自定义,相关实现,可查看内部实现FlutterSmartDialog内部已进行相关实现,使用
show()方法中的isUseExtraWidget区分
最后
这个库花了一些时间去构思实现,算是解决几个很大的痛点
- 如果大家对
返回事件有什么好的处理思路,麻烦在评论里告知,谢谢!
FlutterSmartDialog一些信息
Github:flutter_smart_dialog
使用效果体验:点我
一种更优雅的Flutter Dialog解决方案的更多相关文章
- PostCSS一种更优雅、更简单的书写CSS方式
Sass团队创建了Compass大大提升CSSer的工作效率,你无需考虑各种浏览器前缀兼,只需要按官方文档的书写方式去写,会得到加上浏览器前缀的代码,如下: .row { @include displ ...
- 少年,是时候换种更优雅的方式部署你的php代码了
让我们来回忆下上次你是怎么发布你的代码的: 1. 先把线上的代码用ftp备份下来 2. 上传修改了的文件 3. 测试一下功能是否正常 4. 网站500了,赶紧用备份替换回去 5. 替换错了/替换漏了 ...
- C#中一种替换switch语句更优雅的写法
今天在项目中遇到了使用switch语句判断条件,但问题是条件比较多,大概有几十个条件,满屏幕的case判断,是否有更优雅的写法替代switch语句呢? 假设有这样的一个场景:商场经常会根据情况采取不同 ...
- 一种比css_scoped和css_module更优雅的避免css命名冲突小妙招
css_scoped 与 css_module 我们知道,简单的class名称容易造成css命名重复,比如你定义一个class: <style> .main { float: left; ...
- MySQL root密码忘记,原来还有更优雅的解法!
一直以来,对于MySQL root密码的忘记,以为只有一种解法-skip-grant-tables. 问了下群里的大咖,第一反应也是skip-grant-tables.通过搜索引擎简单搜索了下,无论是 ...
- 用Assert(断言)封装异常,让代码更优雅(附项目源码)
有关Assert断言大家并不陌生,我们在做单元测试的时候,看业务事务复合预期,我们可以通过断言来校验,断言常用的方法如下: public class Assert { /** * 结果 = 预期 则正 ...
- 这一次,解决Flutter Dialog的各种痛点!
前言 Q:你一生中闻过最臭的东西,是什么? A:我那早已腐烂的梦. 兄弟萌!!!我又来了! 这次,我能自信的对大家说:我终于给大家带了一个,能真正帮助大家解决诸多坑比场景的pub包! 将之前的flut ...
- 如何更优雅地对接第三方API
本文所有示例完整代码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/third 我们在日常开发过程 ...
- 使用 Promises 编写更优雅的 JavaScript 代码
你可能已经无意中听说过 Promises,很多人都在讨论它,使用它,但你不知道为什么它们如此特别.难道你不能使用回调么?有什么了特别的?在本文中,我们一起来看看 Promises 是什么以及如何使用它 ...
随机推荐
- 我的第三次C语言作业
我的第三次C语言作业 这个作业属于哪个课程 https://edu.cnblogs.com/campus/zswxy/SE2020-2 这个作业要求在哪里 https://edu.cnblogs.co ...
- Java_垃圾回收机制(未掌握)
垃圾回收机制 任何一种垃圾回收算法一般要做两件基本事情: 发现无用的对象(没有任何变量引用该对象) 回收无用对象占用的内存空间 垃圾回收相关算法: 引用计数法, 引用可达法 分代垃圾回收机制: 不同的 ...
- 【Kata Daily 190927】Counting sheep...(数绵羊)
题目: Consider an array of sheep where some sheep may be missing from their place. We need a function ...
- Thinkphp3.2 cms之分类管理
四.分类管理 <?php namespace Admin\Controller; use Think\Controller; class CateController extends Contr ...
- 面向初学者的Python爬虫程序教程之动态网页抓取
目的是对所有注释进行爬网. 下面列出了已爬网链接.如果您使用AJAX加载动态网页,则有两种方式对其进行爬网. 分别介绍了两种方法:(如果对代码有任何疑问,请提出改进建议)解析真实地址爬网示例是参考链接 ...
- SPI的学习和ESP8266的SPI通讯测试
SPI简介: SPI是串行外设接口(Serial Peripheral Interface)的缩写.SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时 ...
- Linux_end
1.ps 查看进程 ps 查看进程 ps aux 查看所有进程 ps -ef 查看所有进程的详细信息 2.pstree 查看进程树 3.top 查看系统的健康状况 4.netstar 显示网络统计信息 ...
- rados put striper功能的调试
前言 之前对于striper这个地方的功能并没研究太多,只是知道这个里面可以以条带方式并行的去写对象,从而加大并发性来提高性能,而默认的条带数目为1,也就是以对象大小去写,并没有条带,所以不是很好感觉 ...
- eclipse 和 myeclipse 字符编码设置
需要设置的几处地方为: Window->Preferences->General ->Content Type 所有 Default encoding 设置为UTF-8 Window ...
- linux系统中重启网卡后网络不通(NetworkManager篇)
一.故障现象 RHEL7.6系统,使用nmcli绑定双网卡后,再使用以下命令重启network服务后主机网络异常,导致无法通过ssh远程登录系统. # systemctl restart networ ...