前言

系统自带的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包,基本该解决的痛点都已解决了,用于实际业务没什么问题

效果

引入

  1. dependencies:
  2. flutter_smart_dialog: ^1.0.1

使用

  • 主入口配置

    • 在主入口这地方需要配置,这样就可以不传BuildContext使用Dialog
    • 只需要在MaterialApp的builder参数处配置下即可
  1. void main() {
  2. runApp(MyApp());
  3. }
  4. class MyApp extends StatelessWidget {
  5. @override
  6. Widget build(BuildContext context) {
  7. return MaterialApp(
  8. home: SmartDialogPage(),
  9. builder: (BuildContext context, Widget child) {
  10. return Material(
  11. type: MaterialType.transparency,
  12. child: FlutterSmartDialog(child: child),
  13. );
  14. },
  15. );
  16. }
  17. }

使用FlutterSmartDialog包裹下child即可,下面就可以愉快的使用SmartDialog了

  • 使用Toast

    • msg:必传信息
    • time:可选,Duration类型
    • alignment:可控制toast位置
    • 如果想使用花里花哨的Toast效果,使用show方法定制就行了,炒鸡简单喔,懒得写,抄下我的ToastWidget,改下属性即可
  1. SmartDialog.instance.showToast('test toast');
  • 使用Loading
  1. //open loading
  2. SmartDialog.instance.showLoading();
  3. //delay off
  4. await Future.delayed(Duration(seconds: 2));
  5. SmartDialog.instance.dismiss();
  • 自定义dialog

    • 使用SmartDialog.instance.show()方法即可,里面含有众多'Temp'为后缀的参数,和下述无'Temp'为后缀的参数功能一致
  1. SmartDialog.instance.show(
  2. alignmentTemp: Alignment.bottomCenter,
  3. clickBgDismissTemp: true,
  4. widget: Container(
  5. color: Colors.blue,
  6. height: 300,
  7. ),
  8. );
  • 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问题

  1. typedef ScaffoldParamVoidCallback = void Function();
  2. class BaseScaffold extends StatefulWidget {
  3. const BaseScaffold({
  4. Key key,
  5. this.appBar,
  6. this.body,
  7. this.floatingActionButton,
  8. this.floatingActionButtonLocation,
  9. this.floatingActionButtonAnimator,
  10. this.persistentFooterButtons,
  11. this.drawer,
  12. this.endDrawer,
  13. this.bottomNavigationBar,
  14. this.bottomSheet,
  15. this.backgroundColor,
  16. this.resizeToAvoidBottomPadding,
  17. this.resizeToAvoidBottomInset,
  18. this.primary = true,
  19. this.drawerDragStartBehavior = DragStartBehavior.start,
  20. this.extendBody = false,
  21. this.extendBodyBehindAppBar = false,
  22. this.drawerScrimColor,
  23. this.drawerEdgeDragWidth,
  24. this.drawerEnableOpenDragGesture = true,
  25. this.endDrawerEnableOpenDragGesture = true,
  26. this.isTwiceBack = false,
  27. this.isCanBack = true,
  28. this.onBack,
  29. }) : assert(primary != null),
  30. assert(extendBody != null),
  31. assert(extendBodyBehindAppBar != null),
  32. assert(drawerDragStartBehavior != null),
  33. super(key: key);
  34. ///系统Scaffold的属性
  35. final bool extendBody;
  36. final bool extendBodyBehindAppBar;
  37. final PreferredSizeWidget appBar;
  38. final Widget body;
  39. final Widget floatingActionButton;
  40. final FloatingActionButtonLocation floatingActionButtonLocation;
  41. final FloatingActionButtonAnimator floatingActionButtonAnimator;
  42. final List<Widget> persistentFooterButtons;
  43. final Widget drawer;
  44. final Widget endDrawer;
  45. final Color drawerScrimColor;
  46. final Color backgroundColor;
  47. final Widget bottomNavigationBar;
  48. final Widget bottomSheet;
  49. final bool resizeToAvoidBottomPadding;
  50. final bool resizeToAvoidBottomInset;
  51. final bool primary;
  52. final DragStartBehavior drawerDragStartBehavior;
  53. final double drawerEdgeDragWidth;
  54. final bool drawerEnableOpenDragGesture;
  55. final bool endDrawerEnableOpenDragGesture;
  56. ///增加的属性
  57. ///点击返回按钮提示是否退出页面,快速点击俩次才会退出页面
  58. final bool isTwiceBack;
  59. ///是否可以返回
  60. final bool isCanBack;
  61. ///监听返回事件
  62. final ScaffoldParamVoidCallback onBack;
  63. @override
  64. _BaseScaffoldState createState() => _BaseScaffoldState();
  65. }
  66. class _BaseScaffoldState extends State<BaseScaffold> {
  67. //上次点击时间
  68. DateTime _lastPressedAt;
  69. @override
  70. Widget build(BuildContext context) {
  71. return WillPopScope(
  72. child: Scaffold(
  73. appBar: widget.appBar,
  74. body: widget.body,
  75. floatingActionButton: widget.floatingActionButton,
  76. floatingActionButtonLocation: widget.floatingActionButtonLocation,
  77. floatingActionButtonAnimator: widget.floatingActionButtonAnimator,
  78. persistentFooterButtons: widget.persistentFooterButtons,
  79. drawer: widget.drawer,
  80. endDrawer: widget.endDrawer,
  81. bottomNavigationBar: widget.bottomNavigationBar,
  82. bottomSheet: widget.bottomSheet,
  83. backgroundColor: widget.backgroundColor,
  84. resizeToAvoidBottomPadding: widget.resizeToAvoidBottomPadding,
  85. resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
  86. primary: widget.primary,
  87. drawerDragStartBehavior: widget.drawerDragStartBehavior,
  88. extendBody: widget.extendBody,
  89. extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
  90. drawerScrimColor: widget.drawerScrimColor,
  91. drawerEdgeDragWidth: widget.drawerEdgeDragWidth,
  92. drawerEnableOpenDragGesture: widget.drawerEnableOpenDragGesture,
  93. endDrawerEnableOpenDragGesture: widget.endDrawerEnableOpenDragGesture,
  94. ),
  95. onWillPop: dealWillPop,
  96. );
  97. }
  98. ///控件返回按钮
  99. Future<bool> dealWillPop() async {
  100. if (widget.onBack != null) {
  101. widget.onBack();
  102. }
  103. //处理弹窗问题
  104. if (SmartDialog.instance.config.isExist) {
  105. SmartDialog.instance.dismiss();
  106. return false;
  107. }
  108. //如果不能返回,后面的逻辑就不走了
  109. if (!widget.isCanBack) {
  110. return false;
  111. }
  112. if (widget.isTwiceBack) {
  113. if (_lastPressedAt == null ||
  114. DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
  115. //两次点击间隔超过1秒则重新计时
  116. _lastPressedAt = DateTime.now();
  117. //弹窗提示
  118. SmartDialog.instance.showToast("再点一次退出");
  119. return false;
  120. }
  121. return true;
  122. } else {
  123. return true;
  124. }
  125. }
  126. }

几个问题解决方案

穿透背景

  • 穿透背景有俩个解决方案,这里都说明下

AbsorbPointer、IgnorePointer

当时想解决穿透暗色背景,和背景后面的控件互动的时候,我几乎立马想到这俩个控件,先了解下这俩个控件吧

  • AbsorbPointer

    • 阻止子树接收指针事件,AbsorbPointer本身可以响应事件,消耗掉事件

    • absorbing 属性(默认true)

      • true:拦截向子Widget传递的事件 false:不拦截
  1. AbsorbPointer(
  2. absorbing: true,
  3. child: Listener(
  4. onPointerDown: (event){
  5. LogUtil.log('+++++++++++++++++++++++++++++++++');
  6. },
  7. )
  8. )
  • IgnorePointer

    • 阻止子树接收指针事件,IgnorePointer本身无法响应事件,其下的控件可以接收到点击事件(父控件)
    • ignoring 属性(默认true)
      • true:拦截向子Widget传递的事件 false:不拦截
  1. IgnorePointer(
  2. ignoring: true,
  3. child: Listener(
  4. onPointerDown: (event){
  5. LogUtil.log('----------------------------------');
  6. },
  7. )
  8. )

分析

  • 这里来分析下,首先AbsorbPointer这个控件是不合适的,因为AbsorbPointer本身会消费触摸事件,事件被AbsorbPointer消费掉,会导致背景后的页面无法获取到触摸事件;IgnorePointer本身无法消费触摸事件,又由于IgnorePointerAbsorbPointer都具有屏蔽子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即可

下面是写的一个验证小示例

  1. class TestLayoutPage extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return _buildBg(children: [
  5. //底下
  6. Listener(
  7. onPointerDown: (event) {
  8. print(context, '底部蓝色区域++++++++');
  9. },
  10. child: Container(
  11. height: 300,
  12. width: 300,
  13. color: Colors.blue,
  14. ),
  15. ),
  16. //上面 事件穿透
  17. Listener(
  18. behavior: HitTestBehavior.translucent,
  19. onPointerDown: (event) {
  20. print(context, '上面红色区域---------');
  21. },
  22. child: Container(
  23. height: 200,
  24. width: 200,
  25. ),
  26. ),
  27. ]);
  28. }
  29. Widget _buildBg({List<Widget> children}) {
  30. return Scaffold(
  31. appBar: AppBar(title: Text('测试布局')),
  32. body: Center(
  33. child: Stack(
  34. alignment: Alignment.center,
  35. children: children,
  36. ),
  37. ),
  38. );
  39. }
  40. }

Toast和Loading冲突

  • 这个问题,从理论上肯定会存在的,因为一般Overlay库只会使用一个OverlayEntry控件,这会导致,全局只能存在一个浮窗布局,Toast本质是一个全局弹窗,Loading也是一个全局弹窗,使用其中一个都会导致另一个消失

  • Toast明显是应该独立于其他弹窗的一个消息提示,封装在网络库中的关闭弹窗的dismiss方法,也会将Toast消息在不适宜的时间关闭,在实际开发中就碰到此问题,只能多引用一个Toast三方库来解决,在规划这个dialog库的时候,就像必须解决此问题

    • 此处内部多使用了一个OverlayEntry来解决该问题,提供了相关参数来分别控制,完美使Toast独立于其它的dialog弹窗
    • 此处只多提供一个OverlayEntryExtra,如果需要更多,可copy本库,自行定义,多增加一个OverlayEntry都会让内部逻辑和方法使用急剧复杂,维护也会变得不可预期,故只多提供一个OverlayEntry
  • FlutterSmartDialog提供OverlayEntryOverlayEntryExtra可以高度自定义,相关实现,可查看内部实现

  • FlutterSmartDialog内部已进行相关实现,使用show()方法中的isUseExtraWidget区分

最后

这个库花了一些时间去构思实现,算是解决几个很大的痛点

  • 如果大家对返回事件有什么好的处理思路,麻烦在评论里告知,谢谢!

FlutterSmartDialog一些信息

一种更优雅的Flutter Dialog解决方案的更多相关文章

  1. PostCSS一种更优雅、更简单的书写CSS方式

    Sass团队创建了Compass大大提升CSSer的工作效率,你无需考虑各种浏览器前缀兼,只需要按官方文档的书写方式去写,会得到加上浏览器前缀的代码,如下: .row { @include displ ...

  2. 少年,是时候换种更优雅的方式部署你的php代码了

    让我们来回忆下上次你是怎么发布你的代码的: 1. 先把线上的代码用ftp备份下来 2. 上传修改了的文件 3. 测试一下功能是否正常 4. 网站500了,赶紧用备份替换回去 5. 替换错了/替换漏了 ...

  3. C#中一种替换switch语句更优雅的写法

    今天在项目中遇到了使用switch语句判断条件,但问题是条件比较多,大概有几十个条件,满屏幕的case判断,是否有更优雅的写法替代switch语句呢? 假设有这样的一个场景:商场经常会根据情况采取不同 ...

  4. 一种比css_scoped和css_module更优雅的避免css命名冲突小妙招

    css_scoped 与 css_module 我们知道,简单的class名称容易造成css命名重复,比如你定义一个class: <style> .main { float: left; ...

  5. MySQL root密码忘记,原来还有更优雅的解法!

    一直以来,对于MySQL root密码的忘记,以为只有一种解法-skip-grant-tables. 问了下群里的大咖,第一反应也是skip-grant-tables.通过搜索引擎简单搜索了下,无论是 ...

  6. 用Assert(断言)封装异常,让代码更优雅(附项目源码)

    有关Assert断言大家并不陌生,我们在做单元测试的时候,看业务事务复合预期,我们可以通过断言来校验,断言常用的方法如下: public class Assert { /** * 结果 = 预期 则正 ...

  7. 这一次,解决Flutter Dialog的各种痛点!

    前言 Q:你一生中闻过最臭的东西,是什么? A:我那早已腐烂的梦. 兄弟萌!!!我又来了! 这次,我能自信的对大家说:我终于给大家带了一个,能真正帮助大家解决诸多坑比场景的pub包! 将之前的flut ...

  8. 如何更优雅地对接第三方API

    本文所有示例完整代码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/third 我们在日常开发过程 ...

  9. 使用 Promises 编写更优雅的 JavaScript 代码

    你可能已经无意中听说过 Promises,很多人都在讨论它,使用它,但你不知道为什么它们如此特别.难道你不能使用回调么?有什么了特别的?在本文中,我们一起来看看 Promises 是什么以及如何使用它 ...

随机推荐

  1. (一)http协议介绍

    HTTP协议详解 (一) 介绍 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本 ...

  2. 第一行代码中RecyclerView添加依赖库问题

    现在更新到 implementation 'com.android.support:recyclerview-v7:29.2.1' 记得点Sync Now来进行同步.

  3. 带货直播源码开发采用MySQL有什么优越性

    MySQL是世界上最流行的开源关系数据库,带货直播源码使用MySQL,可实现分钟级别的数据库部署和弹性扩展,不仅经济实惠,而且稳定可靠,易于运维.云数据库 MySQL 提供备份恢复.监控.容灾.快速扩 ...

  4. 14 RPC

    14 RPC RPC(Remote Procedure Call Protocol)--远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.RPC协议假定某些 ...

  5. List/Set 泛型转换

    Type typeSet = new TypeToken<Set<Long>>() {}.getType(); Type typeList = new TypeToken< ...

  6. 第三方库文件Joi对数据进行验证的方法以及解决Joi.validate is not a function的问题

    Joi:javaScript对象的规则描述语言和验证器 1.npm install joi@14.3.1 2.建立joi.js文件 3.导入第三方包joi const Joi = require('j ...

  7. 应对告警风暴,Cloud Alert 实现告警风暴智能降噪

    前言 睿象云前段时间发表了一篇< Zabbix 实现电话.邮件.微信告警通知的实践分享>的技术文章.它帮助我们非常轻松地支持了各种告警通知方式,但是存在一个严重的问题,我们经常接到各种相类 ...

  8. 4.Spring Boot web开发

    1.创建一个web模块 (1).创建SpringBoot应用,选中我们需要的模块: (2).SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来 (3).自己编 ...

  9. 一个工作三年左右的Java程序员和大家谈谈从业心得

    转发链接地址:https://mp.weixin.qq.com/s/SSh9HcA5PgMHv7xiolQkig 貌似这一点适应的行业最广,但是我可以很肯定的说:当你从事web开发一年后,重新找工作时 ...

  10. Kubernetes K8S之Taints污点与Tolerations容忍详解

    Kubernetes K8S之Taints污点与Tolerations容忍详解与示例 主机配置规划 服务器名称(hostname) 系统版本 配置 内网IP 外网IP(模拟) k8s-master C ...