Flutter 实现应用内部的自定义widget通知功能

1,894 阅读4分钟

介绍

现在的通知权限不好搞了,所以一些应用增加了内部通知,即:类似通知的显示方式,但是只能在应用(前台状态下)内显示,其本质是一个view。

接下来我们就实现一个可以将我们自定义的widget以通知的形式显示出来的功能。

此功能已加入Bedrock 开发框架 
支持单个/批量显示通知

Flutter Bedrock 快速开发框架 Mvvm+Provider

样式如下:

在这里插入图片描述

实现

INotification

首先,我们先定义一下通知相关的控制行为INotification

abstract class INotification{
	//显示一个通知
  Future showNotificationFromTop({@required Widget child,Duration animationDuration, Duration notifyDwellTime});
  //显示一批通知
  Future showNotifyListFromTop({@required List<Widget> children,Duration animationDuration, Duration notifyDwellTime});
  //预留方法
  Future showNotificationCustom({@required Widget child,Duration animationDuration, Duration notifyDwellTime});
  //增加/移除监听器等,可以监听一个通知的运行状态( 运行中,运行完毕)
  void addNotifyListener(NotifyStatusListener listener);
  void removeNotifyListener(NotifyStatusListener listener);
  void clearAllListener();
}

行为定义好了,我们开始着手实现它

NotificationHandler 显示单个通知

我们通过NotificationHandler 实现上面定义的方法。

先看showNotificationFromTop 这个方法:

显示一个通知
  /// @param [animationDuration] child 从顶部滑出/收回所需时间
  /// @param [notifyDwellTime]  通知 停留时间
  @override
  Future showNotificationFromTop({@required Widget child,Duration animationDuration, Duration notifyDwellTime}) async{
  ///通过 .then等,也可以监听动画状态.
    Completer completer = Completer();
    ///自定义类,增加overlay 和 child的联系
    NotifyOverlayEntry notifyOverlayEntry = NotifyOverlayEntry(child,
        animationDuration??Duration(milliseconds: 500),
        notifyDwellTime??Duration(seconds: 2000),callback: (){
        completer.complete();
        //这里后面会讲到
        if(!streamDone){
          _subscription.resume();
        }
        ///通知结束回调
         _notifyListener(NotifyStatus.Completed);
        });
    /// assume [.insert] is start notify in [NotifyStatus.Running].
    _notifyListener(NotifyStatus.Running);
    ///通过overlay 来添加通知
    Overlay.of(context).insert(notifyOverlayEntry.overlayEntry);
    return completer.future;
  }

为了增加 overlay和child(通知里显示的widget)的联系,这里定义了一个NotifyOverlayEntry类。

NotifyOverlayEntry

class NotifyOverlayEntry{
  final Widget notifyWidget;

  final Duration animationDuration;
  final Duration notifyDwellTime;
  final VoidCallback callback;

  OverlayEntry entry;
  bool notifyDone = false;

  OverlayEntry get  overlayEntry => entry;

  NotifyOverlayEntry(this.notifyWidget, this.animationDuration, this.notifyDwellTime,{@required this.callback,
    NotifyType notifyType = NotifyType.FromTop}){
    ///根据类型 构建不同显示方式的通知
    ///目前只有一个从顶部滑出的方式
    ///如果需要拓展,请务必遵从下面的设计方式
    switch(notifyType){
      case NotifyType.FromTop:
        entry = OverlayEntry(builder: (ctx){
          return FromTopNotifyWidget(notifyWidget,(notifyDone){
            this.notifyDone = notifyDone;
            if(notifyDone) overlayEntry.remove();
            callback();
          },
              animationDuration,notifyDwellTime).generateWidget();
        });
        break;
    }
  }



}

代码很简单,根据通知显示方式生成对应的overlayEntry, 具体的滑出动画由FromTopNotifyWidget来负责。

FromTopNotifyWidget

其内部实现非常简单,只是对我们的child做了一个位移的动画。

从屏幕上(外部)滑入屏幕内

代码如下:

class FromTopNotifyWidget extends WidgetState with SingleTickerProviderStateMixin {


  final Widget child;
  final Duration animationDuration;
  final Duration notifyDwellTime;
  ///动画弹出并收回后,会执行这个回调
  final NotifyDone notifyDone;

  AnimationController controller;
  Animation animation;

  FromTopNotifyWidget(this.child,this.notifyDone, this.animationDuration, this.notifyDwellTime);


  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controller = AnimationController(vsync: this,duration: animationDuration);
    animation = Tween<Offset>(begin: Offset(0,-1),end: Offset.zero).animate(controller);
    controller.addStatusListener((status) {
      if(status == AnimationStatus.completed){
        Future.delayed(notifyDwellTime)
            .whenComplete(() => controller?.reverse()?.whenComplete(() => notifyDone(true)));
      }
    });
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      controller.forward();
    });
  }
  @override
  void dispose() {
    controller?.dispose();
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [AnimatedBuilder(
        animation: animation,
        builder: (ctx,c){
          return SlideTransition(
            position:animation ,
            child: child,);
        },
      )],
    );
  }

}

至此,显示单个通知的功能就实现了,使用方法如下:

          buildBtn('弹出通知', (){
            NotificationHandler(context)
                ..showNotificationFromTop(notifyDwellTime: Duration(seconds: 2),
                  child: buildNotifyChild('notification'),);
          }),

点击按钮后,样式如下:

在这里插入图片描述

当服务器有多个通知需要顺序显示时,该如何实现呢? 我们继续向下看

NotificationHandler 批量显示通知

这里我们需要用到stream,先初始化一波:

///通知是否显示完毕
  bool streamDone = true;

  StreamController<NotifyListItemWrapper> _streamController;
  StreamSink<NotifyListItemWrapper> _sink ;
  StreamSubscription<NotifyListItemWrapper> _subscription;
  NotificationHandler._(this.context){
  ///在构造函数内进行初始化
    _streamController =  StreamController<NotifyListItemWrapper>();
    _sink = _streamController.sink;
    ///进行监听
    _subscription = _streamController.stream.listen((event) {
      if(event == null){
      ///当事件为空时,说明批量通知显示完毕
        streamDone = true;
        if(!_subscription.isPaused){
          _subscription.pause();
        }
        if(listCompleter != null){
          listCompleter.complete();
          listCompleter = null;
        }
        return ;
      }
      ///确保通知挨个进行
      _subscription.pause();
      ///这里使用 【显示单个通知】的方法
      showNotificationFromTop(child: event.child,animationDuration: event.animationDuration,notifyDwellTime: event.notifyDwellTime);
    });

    _streamController.done.then((v) {
      streamDone = true;
    });
    _subscription.pause();



  }

为了方便事件数据的传递,这里额外增加了一个NotifyListItemWrapper类,代码如下:

class NotifyListItemWrapper{
  final Widget child;
  final Duration animationDuration;
  final Duration notifyDwellTime;


  NotifyListItemWrapper(this.child, this.animationDuration, this.notifyDwellTime);
}

主要是对child和动画事件做一个包裹。

接下来看一下,批量显示通知【showNotifyListFromTop】的方法如何实现的:

  Completer listCompleter;

  @override
  Future showNotifyListFromTop({List<Widget> children, Duration animationDuration, Duration notifyDwellTime})async {
    listCompleter = Completer();
    streamDone = false;
    ///先将stream恢复
    _subscription.resume();
    children.forEach((element) {
    ///批量添加数据
      _sink.add(NotifyListItemWrapper(element,animationDuration,notifyDwellTime));
    });
    ///发送一个空数据,以通知事件传送完毕
    _sink.add(null);

    return listCompleter.future;
  }

很简单,至此我们就实现了批量通知显示的方法,下面看一下如何使用:

          buildBtn('弹出多个通知', (){
            NotificationHandler(context)
              ..showNotifyListFromTop(notifyDwellTime: Duration(seconds: 2),
                children:List<Widget>.generate(3, (index) => buildNotifyChild('notification $index')),)
                .whenComplete(() => debugPrint('通知弹出完毕'));
          }),

点击按钮后的样式:

在这里插入图片描述

至此整个功能就实现了,谢谢大家阅读,如果有错误或者更好的方法,欢迎回复交流。

Demo地址

Flutter Bedrock 快速开发框架,内含Demo

系列文章

Flutter 仿网易云音乐App(基础版)

Flutter版 仿.知乎列表的视差效果

Flutter——实现网易云音乐的渐进式卡片切换

Flutter 仿同花顺自选股列表