美文网首页All in FlutterFlutter
Flutter之使用Overlay创建全局Toast并静态调用

Flutter之使用Overlay创建全局Toast并静态调用

作者: NightFarmer | 来源:发表于2018-12-19 11:49 被阅读44次

    Toast在Android上是最常用的提示组件了,它的优势在于静态调用、全局显示,可以在任意你想要的地方调用他而丝毫不影响界面的布局,调用简单程度与Logger的调用不相上下。
    然而在Flutter中并没有给我们提供Toast的接口,想要实现Toast的效果有两种途径,一种是接Android/iOS原生工程,第二种是不依托于使用Flutter来实现。
    本篇选用第二种方案来实现,接原生代码一方面要求双端开发工作量和门槛都较大,而且不利于以后的样式扩展,二是纯Flutter实现的Toast确实效果非常好,自定义样式也非常的方便。使用Flutter相对于RN来说,Flutter的渲染引擎是非常强大的,基本上能用Flutter实现的效果都不建议接原生,而RN则没有自己的渲染引擎,性能的限制造成RN需要频繁的接入原生模块,这也是我倾心Flutter的原因。

    效果图

    本篇要用的核心组件是Overlay,这个组件提供了动态的在Flutter的渲染树上插入布局的特性,从而让我们有了在包括路由在内的所有组件的上层插入toast的可能性。

    创建Flutter工程

    本品系列的Flutter博客都会以创建纯净的Flutter工程开篇,创建工程后,放一个Button在布局中,便于触发Toast调用。
    代码:略。

    使用Overlay插入Toast布局

    因为我们要实现全局的静态调用,所以这里先创建一个工具类,并在这个类中创建静态方法show:

    class Toast {
        
        static show(BuildContext context, String msg) {
            //这里实现toast的弹出逻辑
        }
    
    }
    

    这是一种很常见的静态调用方式,是需要在你的某个回调中调用Toast.show(context, "你的消息提示");即可完成toast的显示,而不用考虑布局嵌套问题。

    下面我们就在show方法中向布局中插入一个toast:

    class Toast {
      static show(BuildContext context, String msg) {
        var overlayState = Overlay.of(context);
        OverlayEntry overlayEntry;
        overlayEntry = new OverlayEntry(builder: (context) {
          return buildToastLayout(msg);
        });
        overlayState.insert(overlayEntry);
      }
    
      static LayoutBuilder buildToastLayout(String msg) {
        return LayoutBuilder(builder: (context, constraints) {
          return IgnorePointer(
            ignoring: true,
            child: Container(
              child: Material(
                color: Colors.white.withOpacity(0),
                child: Container(
                  child: Container(
                    child: Text(
                      "${msg}",
                      style: TextStyle(color: Colors.white),
                    ),
                    decoration: BoxDecoration(
                      color: Colors.black.withOpacity(0.6),
                      borderRadius: BorderRadius.all(
                        Radius.circular(5),
                      ),
                    ),
                    padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
                  ),
                  margin: EdgeInsets.only(
                    bottom: constraints.biggest.height * 0.15,
                    left: constraints.biggest.width * 0.2,
                    right: constraints.biggest.width * 0.2,
                  ),
                ),
              ),
              alignment: Alignment.bottomCenter,
            ),
          );
        });
      }
    }
    
    

    在show方法中使用Overlay插入了一个OverlayEntry,而OverlayEntry负责构建布局,buildToastLayout方法这是一个正常的布局构建方法,通过这个方法我们构建了一个Toast样式的ToastView,并通过OverlayEntry插入到了整个布局的最上层。
    这时候通过调用Toast.show方法就能在界面上看到一个Toast样式的提示了。
    但是,这个ToastView是不会消失的,它会一直呆在界面上,这显然不是我们想要的。

    让Toast自动消失

    我们继续改造这个Toast,让它能够自动消失。
    创建一个叫做ToastView的类,便于控制每次插入的ToastView:

    class ToastView {
      OverlayEntry overlayEntry;
      OverlayState overlayState;
      bool dismissed = false;
    
      _show() async {
        overlayState.insert(overlayEntry);
        await Future.delayed(Duration(milliseconds: 3500));
        this.dismiss();
      }
    
      dismiss() async {
        if (dismissed) {
          return;
        }
        this.dismissed = true;
        overlayEntry?.remove();
      }
    }
    

    这样,就把ToastView的显示和消失的控制封装起来了。然后在Toast的show方法中对他进行调用

    class Toast {
      static show(BuildContext context, String msg) {
        var overlayState = Overlay.of(context);
        OverlayEntry overlayEntry;
        overlayEntry = new OverlayEntry(builder: (context) {
          return buildToastLayout(msg);
        });
        var toastView = ToastView();
        toastView.overlayState = overlayState;
        toastView.overlayEntry = overlayEntry;
        toastView._show();
      }
      ...
    }
    

    通过上面的方法,已经实现了Toast的全局静态调用,并插入全局布局,并在显示3.5秒后自动消失的Toast,但是这个toast好像怪怪的,没错,他没有动画,下面来给这个toast增加动画。

    给Toast增加动画

    这个Toast的动画算是Flutter的高级应用了,它涉及到了缩放,位移,自定义差值器,AnimatedBuilder等特性,本篇的核心在介绍Overlay的使用和ToastView的封装,关于动画的使用如果在这里讲就发散的太多了,篇幅限制以后单独来讲动画吧,这里以你对动画系统了解的前提来讲解。

    class Toast {
      static show(BuildContext context, String msg) {
        var overlayState = Overlay.of(context);
        var controllerShowAnim = new AnimationController(
          vsync: overlayState,
          duration: Duration(milliseconds: 250),
        );
        var controllerShowOffset = new AnimationController(
          vsync: overlayState,
          duration: Duration(milliseconds: 350),
        );
        var controllerHide = new AnimationController(
          vsync: overlayState,
          duration: Duration(milliseconds: 250),
        );
        var opacityAnim1 =
            new Tween(begin: 0.0, end: 1.0).animate(controllerShowAnim);
        var controllerCurvedShowOffset = new CurvedAnimation(
            parent: controllerShowOffset, curve: _BounceOutCurve._());
        var offsetAnim =
            new Tween(begin: 30.0, end: 0.0).animate(controllerCurvedShowOffset);
        var opacityAnim2 = new Tween(begin: 1.0, end: 0.0).animate(controllerHide);
    
        OverlayEntry overlayEntry;
        overlayEntry = new OverlayEntry(builder: (context) {
          return ToastWidget(
            opacityAnim1: opacityAnim1,
            opacityAnim2: opacityAnim2,
            offsetAnim: offsetAnim,
            child: buildToastLayout(msg),
          );
        });
        var toastView = ToastView();
        toastView.overlayEntry = overlayEntry;
        toastView.controllerShowAnim = controllerShowAnim;
        toastView.controllerShowOffset = controllerShowOffset;
        toastView.controllerHide = controllerHide;
        toastView.overlayState = overlayState;
        preToast = toastView;
        toastView._show();
      }
      ...
    }
    
    class ToastView {
      OverlayEntry overlayEntry;
      AnimationController controllerShowAnim;
      AnimationController controllerShowOffset;
      AnimationController controllerHide;
      OverlayState overlayState;
      bool dismissed = false;
    
      _show() async {
        overlayState.insert(overlayEntry);
        controllerShowAnim.forward();
        controllerShowOffset.forward();
        await Future.delayed(Duration(milliseconds: 3500));
        this.dismiss();
      }
    
      dismiss() async {
        if (dismissed) {
          return;
        }
        this.dismissed = true;
        controllerHide.forward();
        await Future.delayed(Duration(milliseconds: 250));
        overlayEntry?.remove();
      }
    }
    
    class ToastWidget extends StatelessWidget {
      final Widget child;
      final Animation<double> opacityAnim1;
      final Animation<double> opacityAnim2;
      final Animation<double> offsetAnim;
    
      ToastWidget(
          {this.child, this.offsetAnim, this.opacityAnim1, this.opacityAnim2});
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: opacityAnim1,
          child: child,
          builder: (context, child_to_build) {
            return Opacity(
              opacity: opacityAnim1.value,
              child: AnimatedBuilder(
                animation: offsetAnim,
                builder: (context, _) {
                  return Transform.translate(
                    offset: Offset(0, offsetAnim.value),
                    child: AnimatedBuilder(
                      animation: opacityAnim2,
                      builder: (context, _) {
                        return Opacity(
                          opacity: opacityAnim2.value,
                          child: child_to_build,
                        );
                      },
                    ),
                  );
                },
              ),
            );
          },
        );
      }
    }
    
    class _BounceOutCurve extends Curve {
      const _BounceOutCurve._();
    
      @override
      double transform(double t) {
        t -= 1.0;
        return t * t * ((2 + 1) * t + 2) + 1.0;
      }
    }
    
    

    这是段非常长的代码,本来是不想往上面贴这么多代码的,但是动画这块儿讲的话篇幅又太长,不贴代码的话讲起来又太空洞,只能贴了,大概说一下。
    上面代码分为四段:
    第一段,在show方法中创建3个动画,Toast显示的位移和渐显动画,Toast消失的渐隐动画,然后把这三个动画的controller交给ToastView来控制动画播放。
    第二段,在ToastView中接收三个动画controller,并在show和dismiss方法中控制动画的播放。
    第三段,创建一个自定义Widget,并使用三个AnimatedBuilder来实现动画,并在show方法中把Toast的布局包裹起来。
    第四段,定义了一个动画差值器,Flutter中提供了很多动画差值器,但是并没有我们需要的,所以这里定义一个弹跳一次后回弹的动画差值器用来控制ToastView的偏移动画效果。

    到目前为止,这个Toast已经满足了最基本的样式,全局调用,动画弹出,延迟3.5秒后自动渐隐消失。

    防止连续调用造成toast堆叠

    但是还存在一个问题,因为Toast的样式的半透明的黑色,如果连续调用多次的话,会有多个Toast同时弹出,并堆叠在一起,会显得非常的黑。

    下面再做一个处理,在show之前,判断是否已经有一个Toast在显示了,如果有,即刻把它dismiss了。

      static ToastView preToast;
    
      static show(BuildContext context, String msg) {
        preToast?.dismiss();
        preToast = null;
        ...
        preToast = toastView;
        toastView._show();
      }
      ...
    }
    
    

    这样就可以了,?.操作符和kotlin的效果是一样的,空指针安全,很舒服。


    更多干货移步我的个人博客 https://www.nightfarmer.top/

    相关文章

      网友评论

        本文标题:Flutter之使用Overlay创建全局Toast并静态调用

        本文链接:https://www.haomeiwen.com/subject/gladkqtx.html