前言
零碎自带的 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: any
应用
-
主入口配置
- 在主入口这中央须要配置下,这样就能够不传 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.showToast('test toast');
-
应用 Loading
- msg:可选,loading 动画上面的文字信息(默认:加载中 …)
//open loading
SmartDialog.showLoading();
//delay off
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();
-
自定义 dialog
- 应用 SmartDialog.instance.show()办法即可,外面含有泛滥 ’Temp’ 为后缀的参数,和下述无 ’Temp’ 为后缀的参数性能统一
- 非凡属性
isUseExtraWidget
:是否应用额定笼罩浮层,可与主浮层独立开;可与 loading,dialog 之类独立开,自带的 showToast 便是开启了该配置,可与 loading 共存
SmartDialog.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.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、IgnorePointer
过后想解决穿透暗色背景,和背景前面的控件互动的时候,我简直立马想到这俩个控件,先理解下这俩个控件吧
-
AbsorbPointer
- 阻止子树接管指针事件,
AbsorbPointer
自身能够响应事件,消耗掉事件 -
absorbing
属性(默认 true)- true:拦挡向子 Widget 传递的事件 false:不拦挡
- 阻止子树接管指针事件,
AbsorbPointer(
absorbing: true,
child: Listener(onPointerDown: (event){print('+++++++++++++++++++++++++++++++++');
},
)
)
-
IgnorePointer
- 阻止子树接管指针事件,
IgnorePointer
自身无奈响应事件,其下的控件能够接管到点击事件(父控件) -
ignoring
属性(默认 true)- true:拦挡向子 Widget 传递的事件 false:不拦挡
- 阻止子树接管指针事件,
IgnorePointer(
ignoring: true,
child: Listener(onPointerDown: (event){print('----------------------------------');
},
)
)
剖析
- 这里来剖析下,首先
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
控件,我这里设置了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,
),
),
);
}
}
Toast 和 Loading 抵触
- 这个问题,从实践上必定会存在的,因为个别 Overlay 库只会应用一个 OverlayEntry 控件,这会导致,全局只能存在一个浮窗布局,Toast 实质是一个全局弹窗,Loading 也是一个全局弹窗,应用其中一个都会导致另一个隐没
-
Toast 显著是应该独立于其余弹窗的一个音讯提醒,封装在网络库中的敞开弹窗的 dismiss 办法,也会将 Toast 音讯在不合适的时候敞开,在理论开发中就碰到此问题,只能多援用一个 Toast 三方库来解决,在布局这个 dialog 库的时候,就想到必须解决此问题
- 此处外部多应用了一个 OverlayEntry 来解决该问题,提供了相干参数来别离管制,完满使 Toast 独立于其它的 dialog 弹窗
- 多减少一个 OverlayEntry 都会让外部逻辑和办法应用急剧简单,保护也会变得不可预期,故额定只多提供一个 OverlayEntry;如果须要更多,可 copy 本库,自行定义,实现该库相干源码,都力求能让人看明确,置信大家 copy 应用时不会感到艰涩难懂
- FlutterSmartDialog 提供
OverlayEntry
和OverlayEntryExtra
能够高度自定义,相干实现,可查看外部实现 - FlutterSmartDialog 外部已进行相干实现,应用
show()
办法中的isUseExtraWidget
辨别
最初
这个库花了一些工夫去构思和实现,算是解决几个很大的痛点
- 如果大家对
返回事件
有什么好的解决思路,麻烦在评论里告知,谢谢!
FlutterSmartDialog 一些信息
- Github:flutter_smart_dialog
- Pub:flutter_smart_dialog
- 应用成果体验:体验一下