前言

零碎自带的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包,根本该解决的痛点都已解决了,用于理论业务没什么问题

成果

  • 点我体验一下

引入

  • Pub:flutter_smart_dialog
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 loadingSmartDialog.instance.showLoading();//delay offawait 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管制自定义控件位于屏幕的地位<br/>Alignment.center: 自定义控件位于屏幕两头,且是动画默认为:渐隐和缩放,可应用isLoading抉择动画<br/>Alignment.bottomCenter、Alignment.bottomLeft、Alignment.bottomRight:自定义控件位于屏幕底部,动画默认为位移动画,自下而上,可应用animationDuration设置动画工夫<br/>Alignment.topCenter、Alignment.topLeft、Alignment.topRight:自定义控件位于屏幕顶部,动画默认为位移动画,自上而下,可应用animationDuration设置动画工夫<br/>Alignment.centerLeft:自定义控件位于屏幕右边,动画默认为位移动画,自左而右,可应用animationDuration设置动画工夫<br/> 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自身无奈生产触摸事件,又因为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即可

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

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提供OverlayEntryOverlayEntryExtra能够高度自定义,相干实现,可查看外部实现
  • FlutterSmartDialog外部已进行相干实现,应用show()办法中的isUseExtraWidget辨别

最初

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

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

FlutterSmartDialog一些信息

  • Github:flutter_smart_dialog
  • Pub:flutter_smart_dialog
  • 应用成果体验:点我