2

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

 2 years ago
source link: https://my.oschina.net/xdd666/blog/5293587
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

> Q:你一生中闻过最臭的东西,是什么? > > A:我那早已腐烂的梦。

兄弟萌!!!我又来了!

这次,我能自信的对大家说:我终于给大家带了一个,能真正帮助大家解决诸多坑比场景的pub包!

将之前的flutter_smart_dialog,在保持api稳定的基础上,进行了各种抓头重构,解决了一系列问题

现在,我终于可以说:它现在是一个简洁,强大,侵入性极低的pub包!

关于侵入性问题

  • 之前为了解决返回关闭弹窗,使用了一个很不优雅的解决方法,导致侵入性有点高
  • 这真是让我如坐针毡,如芒刺背,如鲠在喉,这个问题终于搞定了!

同时,我在pub包内部设计了一个弹窗栈,能自动移除栈顶弹窗,也可以定点移除栈内标记的弹窗。

使用系统弹窗存在一系列坑,来和各位探讨探讨

  • 必须传BuildContext

    • 在一些场景必须多做一些传参工作,蛋痛但不难的问题
  • loading弹窗

    • 使用系统弹窗做loading弹窗,肯定遇到过这个坑比问题
      • loading封装在网络库里面:请求网络时加载loading,手贱按了返回按钮,关闭了loading
      • 然后请求结束后发现:特么我的页面怎么被关了!!!
    • 系统弹窗就是一个路由页面,关闭系统就是用pop方法,这很容易误关正常页面
      • 当然肯定有解决办法,路由监听的地方处理,此处就不细表了
  • 某页面弹出了多个系统Dialog,很难定点关闭某个非栈顶弹窗

    • 蛋蛋,这是路由入栈出栈机制导致的,理解的同时也一样要吐槽
  • 系统Dialog,点击事件无法穿透暗色背景

    • 这个坑比问题,我是真没辙

上面列举了一些比较常见的问题,最严重的问题,应该就是loading的问题

  • loading是个超高频使用的弹窗,关闭loading弹窗的方法,同时也能关闭正常使用的页面,本身就是一个隐患
  • 本菜狗不具备大厂大佬们魔改flutter的能力,菜则思变,我只能从其它方向切入,寻求解决方案

系统的Page就是基于Overlay去实现的,咱们也要骚起来,从Overlay入手

这次,我要一次性帮各位解决:toast消息,loading弹窗,以及更强大的自定义dialog!

dependencies:
  flutter_smart_dialog: ^3.0.0

> 初始化方式一:强力推荐😃

  • 配置更加简洁
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialRoute: RouteConfig.main,
      getPages: RouteConfig.getPages,
      // here
      navigatorObservers: [FlutterSmartDialog.observer],
      // here
      builder: FlutterSmartDialog.init(),
    );
  }
}

> 初始化方式二:兼容老版本

  • 老版本初始化方式仍然有效,区别是:需要在就加载MaterialApp之前,调用下FlutterSmartDialog.monitor()
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // here
    FlutterSmartDialog.monitor();
    return MaterialApp(
      home: SmartDialogPage(),
      // here
      navigatorObservers: [FlutterSmartDialog.observer],
      /// here
      builder: (BuildContext context, Widget? child) {
        return FlutterSmartDialog(child: child);
      },
    );
  }
}

> 大功告成🚀

上面俩种初始化方式,任选一种即可;然后,就可以完整使用本库的所有功能了

我非常推荐第一种初始化的方式,因为足够简洁;简洁明了的东西用起来,会让人心情愉悦🌞

  • toast使用💬
SmartDialog.showToast('test toast');

toastDefault

  • loading使用
SmartDialog.showLoading();
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss(); 

loadingDefault

  • dialog使用🎨
var custom = Container(
    height: 80,
    width: 180,
    decoration: BoxDecoration(
        color: Colors.black,
        borderRadius: BorderRadius.circular(20),
    ),
    alignment: Alignment.center,
    child: Text('easy custom dialog', style: TextStyle(color: Colors.white)),
);
// here
SmartDialog.show(widget: custom, isLoadingTemp: false);

dialogEasy

OK,上面展示了,只需要极少的代码,就可以调用相应的功能

当然,内部还有不少地方做了特殊优化,接下来,我会详细的向大家描述下

你可能会有的疑问

初始化框架的时候,相比以前,居然让大家多写了一个参数,内心十分愧疚😩

关闭页面本质上是一个比较复杂的情况,涉及到

  • 物理返回按键
  • AppBar的back按钮
  • 手动pop

为了监控这些情况,不得已增加了一个路由监控参数

> 关于 FlutterSmartDialog.init()

本方法不会占用你的builder参数,init内部回调出来了builder,你可以大胆放心的继续套

  • 例如:继续套Bloc全局实例😄
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialRoute: RouteConfig.main,
      getPages: RouteConfig.getPages,
      navigatorObservers: [FlutterSmartDialog.observer],
      builder: FlutterSmartDialog.init(builder: _builder),
    );
  }
}

Widget _builder(BuildContext context, Widget? child) {
  return MultiBlocProvider(
    providers: [
      BlocProvider.value(value: BlocSpanOneCubit()),
    ],
    child: child!,
  );
}

> 实体返回键

对返回按钮的监控,是非常重要的,基本能覆盖大多数情况

initBack

> pop路由

虽然对返回按钮的监控能覆盖大多数场景,但是一些手动pop的场景就需要新增参数监控

  • 不加FlutterSmartDialog.observer
    • 如果打开了穿透参数(就可以和弹窗后的页面交互),然后手动关闭页面
    • 就会出现这种很尴尬的情况

initPopOne

  • 加了FlutterSmartDialog.observer,就能比较合理的处理了
    • 当然,这里的过渡动画,也提供了参数控制是否开启❤

initPopTwo

> 超实用的参数:backDismiss

  • 这个参数是默认设置为true,返回的时候会默认关闭弹窗;如果设置为false,将不会关闭页面
    • 这样就可以十分轻松的做一个紧急弹窗,禁止用户的下一步操作
  • 我们来看一个场景:假定某开源作者决定弃坑软件,不允许用户再使用该软件的弹窗
SmartDialog.show(
  // here
  backDismiss: false,
  clickBgDismissTemp: false,
  isLoadingTemp: false,
  widget: Container(
    height: 480,
    width: 500,
    padding: EdgeInsets.all(20),
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(20),
      color: Colors.white,
    ),
    alignment: Alignment.topCenter,
    child: SingleChildScrollView(
      child: Wrap(
        direction: Axis.vertical,
        crossAxisAlignment: WrapCrossAlignment.center,
        spacing: 10,
        children: [
          // title
          Text(
            '特大公告',
            style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
          ),
          // content
          Text('鄙人日夜钻研下面秘籍,终于成功钓到富婆'),
          Image.network(
            'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211102213746.jpeg',
            height: 200,
            width: 400,
          ),
          Text('鄙人思考了三秒钟,怀着\'沉重\'的心情,决定弃坑本开源软件'),
          Text('本人今后的生活是富婆和远方,已无\'精力\' 再维护本开源软件了'),
          Text('各位叼毛,有缘江湖再见!'),
          // button (only method of close the dialog)
          ElevatedButton(
            onPressed: () => SmartDialog.dismiss(),
            child: Text('再会!'),
          )
        ],
      ),
    ),
  ),
);

hardClose

从上面的效果图可以看出来

  • 点击遮罩,无法关闭弹窗
  • 点击返回按钮无法关闭弹窗
  • 只能点我们自己的按钮,才能关闭弹窗,点击按钮的逻辑可以直接写成关闭app之类

只需要俩个简单的参数设置,就能实现这样一个很棒的应急弹窗

> 设置全局参数

SmartDialog的全局参数都有着一个比较合理的默认值

为了应付多变的场景,你可以修改符合你自己要求的全局参数

  • 设置符合你的要求的数据,放在app入口就行初始化就行
    • 注:如果没有特殊要求,完全可以不用初始化全局参数
SmartDialog.config
    ..alignment = Alignment.center
    ..isPenetrate = false
    ..clickBgDismiss = true
    ..maskColor = Colors.black.withOpacity(0.3)
    ..maskWidget = null
    ..animationDuration = Duration(milliseconds: 260)
    ..isUseAnimation = true
    ..isLoading = true;
  • 代码的注释写的很完善,某个参数不明白的,点进去看看就行了

image-20211102223129866

Toast篇

toast的特殊性

严格来说,toast是一个非常特殊的弹窗,我觉得理应具备下述的特征

> toast消息理应一个个展示,后续消息不应该顶掉前面的toast

  • 这是一个坑点,如果框架内部不做处理,很容易出现后面toast会直接顶掉前面toast的情况

toastOne

> 展示在页面最上层,不应该被一些弹窗之类遮挡

  • 可以发现loading和dialog的遮罩等布局,均未遮挡toast信息

toastTwo

> 对键盘遮挡情况做处理

  • 键盘这玩意有点坑,会直接遮挡所有布局,只能曲线救国
    • 在这里做了一个特殊处理,当唤起键盘的时候,toast自己会动态的调整自己和屏幕底部的距离
    • 这样就能起到一个,键盘不会遮挡toast的效果

toastSmart

自定义Toast

> 参数说明

toast暴露的参数其实并不多,只提供了四个参数

  • 例如:toast字体大小,字体颜色,toast的背景色等等之类,我都没提供参数
    • 一是觉得提供了这些参数,会让整个参数输入变的非常多,乱花渐入迷人眼
    • 二是觉得就算我提供了很多参数,也不一定会满足那些奇奇怪怪的审美和需求
  • 基于上述的考虑,我直接提供了底层参数,直接将widget参数提供出来
    • 你可以随心所欲的定制toast了
    • 注意:使用了widget参数,msgalignment参数会失效

image-20211031155838900

> 调整toast显示的位置

SmartDialog.showToast('the toast at the bottom');
SmartDialog.showToast('the toast at the center', alignment: Alignment.center);
SmartDialog.showToast('the toast at the top', alignment: Alignment.topCenter);

toastLocation

> 更强大的自定义toast

  • 首先,整一个自定义toast
class CustomToast extends StatelessWidget {
  const CustomToast(this.msg, {Key? key}) : super(key: key);

  final String msg;

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        margin: EdgeInsets.only(bottom: 30),
        padding: EdgeInsets.symmetric(horizontal: 20, vertical: 7),
        decoration: BoxDecoration(
          color: _randomColor(),
          borderRadius: BorderRadius.circular(100),
        ),
        child: Row(mainAxisSize: MainAxisSize.min, children: [
          //icon
          Container(
            margin: EdgeInsets.only(right: 15),
            child: Icon(Icons.add_moderator, color: _randomColor()),
          ),

          //msg
          Text('$msg', style: TextStyle(color: Colors.white)),
        ]),
      ),
    );
  }

  Color _randomColor() {
    return Color.fromRGBO(
      Random().nextInt(256),
      Random().nextInt(256),
      Random().nextInt(256),
      1,
    );
  }
}
SmartDialog.showToast('', widget: CustomToast('custom toast'));

toastCustom

Loading篇

  • 开启loading后,可以使用以下方式关闭
    • SmartDialog.dismiss():可以关闭loading和dialog
    • status设置为SmartStatus.loading:仅仅关闭loading
// easy close
SmartDialog.dismiss();
// exact close
SmartDialog.dismiss(status: SmartStatus.loading);
  • 一般来说,loading弹窗是封装在网络库里面的,随着请求状态的自动开启和关闭
    • 基于这种场景,我建议:使用dismiss时,加上status参数,将其设置为:SmartStatus.loading
  • 坑比场景
    • 网络请求加载的时候,loading也随之打开,这时很容易误触返回按钮,关闭loading
    • 当网络请求结束时,会自动调用dismiss方法
    • 因为loading已被关闭,假设此时页面又有SmartDialog的弹窗,未设置status的dismiss就会关闭SmartDialog的弹窗
    • 当然,这种情况很容易解决,封装进网络库的loading,使用:SmartDialog.dismiss(status: SmartStatus.loading); 关闭就行了
  • status参数,是为了精确关闭对应类型弹窗而设计的参数,在一些特殊场景能起到巨大的作用
    • 如果大家理解这个参数的含义,那对于何时添加status参数,必能胸有成竹

参数在注释里面写的十分详细,就不赘述了,来看看效果

image-20211031215728656

  • maskWidgetTemp:强大的遮罩自定义功能😆,发挥你的脑洞吧。。。
var maskWidget = Container(
  width: double.infinity,
  height: double.infinity,
  child: Opacity(
    opacity: 0.6,
    child: Image.network(
      'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101103911.jpeg',
      fit: BoxFit.fill,
    ),
  ),
);
SmartDialog.showLoading(maskWidgetTemp: maskWidget);

loadingOne

  • maskColorTemp:支持快捷自定义遮罩颜色
SmartDialog.showLoading(maskColorTemp: randomColor().withOpacity(0.3));

/// random color
Color randomColor() => Color.fromRGBO(
    Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

loadingTwo

  • background:支持加载背景自定义
SmartDialog.showLoading(background: randomColor());

/// random color
Color randomColor() => Color.fromRGBO(
    Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

loadingThree

  • isLoadingTemp:动画效果切换
SmartDialog.showLoading(isLoadingTemp: false);

loadingFour

  • isPenetrateTemp:交互事件可以穿透遮罩,这是个十分有用的功能,对于一些特殊的需求场景十分关键
SmartDialog.showLoading(isPenetrateTemp: true);

loadingFive

自定义Loading

使用showLoading可以轻松的自定义出强大的loading弹窗;鄙人脑洞有限,就简单演示下

> 自定义一个loading布局

class CustomLoading extends StatefulWidget {
  const CustomLoading({Key? key, this.type = 0}) : super(key: key);

  final int type;

  @override
  _CustomLoadingState createState() => _CustomLoadingState();
}

class _CustomLoadingState extends State<customloading>
    with TickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );
    _controller.forward();
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reset();
        _controller.forward();
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(children: [
      // smile
      Visibility(visible: widget.type == 0, child: _buildLoadingOne()),

      // icon
      Visibility(visible: widget.type == 1, child: _buildLoadingTwo()),

      // normal
      Visibility(visible: widget.type == 2, child: _buildLoadingThree()),
    ]);
  }

  Widget _buildLoadingOne() {
    return Stack(alignment: Alignment.center, children: [
      RotationTransition(
        alignment: Alignment.center,
        turns: _controller,
        child: Image.network(
          'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101174606.png',
          height: 110,
          width: 110,
        ),
      ),
      Image.network(
        'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101181404.png',
        height: 60,
        width: 60,
      ),
    ]);
  }

  Widget _buildLoadingTwo() {
    return Stack(alignment: Alignment.center, children: [
      Image.network(
        'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101162946.png',
        height: 50,
        width: 50,
      ),
      RotationTransition(
        alignment: Alignment.center,
        turns: _controller,
        child: Image.network(
          'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101173708.png',
          height: 80,
          width: 80,
        ),
      ),
    ]);
  }

  Widget _buildLoadingThree() {
    return Center(
      child: Container(
        height: 120,
        width: 180,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(15),
        ),
        alignment: Alignment.center,
        child: Column(mainAxisSize: MainAxisSize.min, children: [
          RotationTransition(
            alignment: Alignment.center,
            turns: _controller,
            child: Image.network(
              'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101163010.png',
              height: 50,
              width: 50,
            ),
          ),
          Container(
            margin: EdgeInsets.only(top: 20),
            child: Text('loading...'),
          ),
        ]),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

> 来看看效果

SmartDialog.showLoading(isLoadingTemp: false, widget: CustomLoading());
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingSmile

SmartDialog.showLoading(
    isLoadingTemp: false,
    widget: CustomLoading(type: 1),
);
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingIcon

SmartDialog.showLoading(widget: CustomLoading(type: 2));
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingNormal

Dialog篇

> 弹窗从不同位置弹出,动画是有区别的

image-20211031221419600

  • alignmentTemp:该参数设置不同,动画效果会有所区别
var location = ({
    double width = double.infinity,
    double height = double.infinity,
}) {
    return Container(width: width, height: height, color: randomColor());
};

//left
SmartDialog.show(
    widget: location(width: 50),
    alignmentTemp: Alignment.centerLeft,
);
await Future.delayed(Duration(milliseconds: 500));
//top
SmartDialog.show(
    widget: location(height: 50),
    alignmentTemp: Alignment.topCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//right
SmartDialog.show(
    widget: location(width: 50),
    alignmentTemp: Alignment.centerRight,
);
await Future.delayed(Duration(milliseconds: 500));
//bottom
SmartDialog.show(
    widget: location(height: 50),
    alignmentTemp: Alignment.bottomCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//center
SmartDialog.show(
    widget: location(height: 100, width: 100),
    alignmentTemp: Alignment.center,
    isLoadingTemp: false,
);

dialogLocation

  • isPenetrateTemp:交互事件穿透遮罩
SmartDialog.show(
    alignmentTemp: Alignment.centerRight,
    isPenetrateTemp: true,
    clickBgDismissTemp: false,
    widget: Container(
        width: 80,
        height: double.infinity,
        color: randomColor(),
    ),
);

dialogPenetrate

dialog栈

  • 这是一个强大且实用的功能!
    • 可以很轻松的定点关闭某个弹窗
var stack = ({
  double width = double.infinity,
  double height = double.infinity,
  String? msg,
}) {
  return Container(
    width: width,
    height: height,
    color: randomColor(),
    alignment: Alignment.center,
    child: Text('弹窗$msg', style: TextStyle(color: Colors.white)),
  );
};

//left
SmartDialog.show(
  tag: 'A',
  widget: stack(msg: 'A', width: 60),
  alignmentTemp: Alignment.centerLeft,
);
await Future.delayed(Duration(milliseconds: 500));
//top
SmartDialog.show(
  tag: 'B',
  widget: stack(msg: 'B', height: 60),
  alignmentTemp: Alignment.topCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//right
SmartDialog.show(
  tag: 'C',
  widget: stack(msg: 'C', width: 60),
  alignmentTemp: Alignment.centerRight,
);
await Future.delayed(Duration(milliseconds: 500));
//bottom
SmartDialog.show(
  tag: 'D',
  widget: stack(msg: 'D', height: 60),
  alignmentTemp: Alignment.bottomCenter,
);
await Future.delayed(Duration(milliseconds: 500));

//center:the stack handler
SmartDialog.show(
  alignmentTemp: Alignment.center,
  isLoadingTemp: false,
  widget: Container(
    decoration: BoxDecoration(
        color: Colors.white, borderRadius: BorderRadius.circular(15)),
    padding: EdgeInsets.symmetric(horizontal: 30, vertical: 20),
    child: Wrap(spacing: 20, children: [
      ElevatedButton(
        child: Text('关闭弹窗A'),
        onPressed: () => SmartDialog.dismiss(tag: 'A'),
      ),
      ElevatedButton(
        child: Text('关闭弹窗B'),
        onPressed: () => SmartDialog.dismiss(tag: 'B'),
      ),
      ElevatedButton(
        child: Text('关闭弹窗C'),
        onPressed: () => SmartDialog.dismiss(tag: 'C'),
      ),
      ElevatedButton(
        child: Text('关闭弹窗D'),
        onPressed: () => SmartDialog.dismiss(tag: 'D'),
      ),
    ]),
  ),
);

dialogStack

骚气的小技巧

有一种场景比较蛋筒

  • 我们使用StatefulWidget封装了一个小组件
  • 在某个特殊的情况,我们需要在这个组件外部,去触发这个组件内部的一个方法
  • 对于这种场景,有不少实现方法,但是弄起来可能有点麻烦

这里提供一个简单的小思路,可以非常轻松的触发,组件内部的某个方法

  • 建立一个小组件
class OtherTrick extends StatefulWidget {
  const OtherTrick({Key? key, this.onUpdate}) : super(key: key);

  final Function(VoidCallback onInvoke)? onUpdate;

  @override
  _OtherTrickState createState() => _OtherTrickState();
}

class _OtherTrickState extends State<othertrick> {
  int _count = 0;

  @override
  void initState() {
    // here
    widget.onUpdate?.call(() {
      _count++;
      setState(() {});
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 50, vertical: 20),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(10),
        ),
        child: Text('Counter: $_count ', style: TextStyle(fontSize: 30.0)),
      ),
    );
  }
}
  • 展示这个组件,然后外部触发它
VoidCallback? callback;

// display
SmartDialog.show(
  alignmentTemp: Alignment.center,
  widget: OtherTrick(
    onUpdate: (VoidCallback onInvoke) => callback = onInvoke,
  ),
);

await Future.delayed(Duration(milliseconds: 500));

// handler
SmartDialog.show(
  alignmentTemp: Alignment.centerRight,
  maskColorTemp: Colors.transparent,
  widget: Container(
    height: double.infinity,
    width: 150,
    color: Colors.white,
    alignment: Alignment.center,
    child: ElevatedButton(
      child: Text('add'),
      onPressed: () => callback?.call(),
    ),
  ),
);
  • 来看下效果

trick

> 相关地址

> 哎,人总是在不断的迷茫中前行。。。

梦

</othertrick></customloading>


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK