Flutter之SnackBar原理详解

初次学习SnackBar控件,第一反应就是这货怎么感觉跟Android的Toast一样!使用起来确实简单,但是其内部原理扒拉出来到时能学到一点东西,下面就细细的剖析这个组件。

Snackbar的作用就是在屏幕的底部展示一个简短的消息,与此同时,Snackbar也可以与用户进行交互,实现效果如下图:
在这里插入图片描述
如上图所示SnackBar分成两个部分:内容区域(content)+交互区域(action)。Scaffold是可以配置底部导航tab的,如果配置了的话,SnackBar怎么展示呢?如下图可以看出SnackBar紧贴着底部导航tab展示:
在这里插入图片描述
上面两图展示SnackBar的代码如下:

  Scaffold.of(context).showSnackBar(SnackBar(
                  content: Text('断网了?'),
                  action: SnackBarAction(
                    label: '点击重试',
                    onPressed: () {
                      //执行相关逻辑
                    },
                  ),
                ));
SnackBar相关属性简介
const SnackBar({
    Key key,
    @required this.content,
    this.backgroundColor,
    this.elevation,
    this.shape,
    this.behavior,
    this.action,
    this.duration = _snackBarDisplayDuration,
    this.animation,
    this.onVisible,
  })

从构造函数来看,SnackBar可以进行如下配置:

属性名类型说明
contentWidgetSnackBar的内容组件,通常使用Text作为content
backgroundColorColor背景颜色,默认为ThemeData.snackBarTheme.backgroundColor
elevationdoublez-coordinate 的值,类似于Card组件的elevation属性
shapeShapeBorder可以设置Snackbar的形状,比如圆角矩形,上图是常规无圆角的矩形
behaviorSnackBarBehavior为枚举类型,有两个值fixed和floating
actionSnackBarActionSnackBarAction是一个Widget类型,用来与用户交互的组件 ,其内部就是一个Text组件,配置该属性必须配置onPressed
durationDurationSnackBar的显示时长
animationAnimationSnackBar的显示和退出时的动画,该属性好像没啥用,因为在showSnackBar的时候被强制的使用自带的animation覆盖掉
onVisibleVoidCallbackSnackBar第一次显示的时候调用

下面就根据上面的属性,配置了一个圆角红色背景,behavior为floating的SnackBar:
在这里插入图片描述

代码如下,可以看出behavior配置为SnackBarBehavior.floating的时候,SnackBar底部并没有紧贴着底部导航tab:

 Scaffold.of(context).showSnackBar(SnackBar(
                  onVisible: () {
                    print("显示SnackBar");
                  },
                  shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.all(Radius.circular(50))
                  ),
                  behavior: SnackBarBehavior.floating,
                  backgroundColor: Colors.red,
                  content: Text('断网了?'),
                  action: SnackBarAction(
                    ///配置action的字体颜色为绿色
                    textColor: Colors.green,
                    label: '点击重试',
                    onPressed: () {
                      //执行相关逻辑
                    },
                  ),
                ));
SnackBar的使用细节:

1、需要结合Scaffold使用,通过Scaffold.of(context)获取到的ScaffoldState对象,然后调用该对象的showSnackBar方法
2‘、action是可选组件,但是如果配置了action组件,那么onPressed必须配置。
3、SnackBar 在现实一段时间后会自动关闭。
4、当点击action区域,比如上图的“点击重试”事,SnackBar会立即关闭
5、当连续调用 Scaffold.of(context).showSnackBar现实多条SnackBar的时候,下一个SnackBar会在上一个SnackBar消失的时候才会展示出来。也就是说其展示逻辑是FIFO的队列形式。如果我们希望新的消息来得时候,旧的消息立即消息,并立即展示新的消息,该怎么办呢?
解决起来也很简单,可以在Scaffold.of(context).showSnackBar之前调用如Scaffold.of(context).removeCurrentSnackBar(),即

//删除之前的SnackBar
Scaffold.of(context).removeCurrentSnackBar()
Scaffold.of(context).showSnackBar(SnackBar);
SnackBar的实现原理:
1、SnackBar关闭的几种原因

SnackBar的使用十分简单,但是深究其原理倒是能学到一点东西,下面就来具体分析其实现原理。我们知道SnackBar在限定时间内会自动关闭,事实上SnackBar关闭的原因有多种,使用SnackBarClosedReason这个枚举类型来表示:

关闭原因说明
action当用户设置了action属性,用户点击action后,会里面关闭SnackBar,本质上是调用 Scaffold.of(context).hideCurrentSnackBar()
dismiss通过执行Semantics组件的onDismiss回调函数来关闭SnackBar ,本质上是调用Scaffold.of(context).removeCurrentSnackBar()
swipe通过执行Dismissible组件的onDismiss回调函数来关闭SnackBar,本质上是调用Scaffold.of(context).removeCurrentSnackBar()
hide通过执行Scaffold.of(context).hideCurrentSnackBar() 关闭SnackBar
remove通过执行Scaffold.of(context).removeCurrentSnackBar() 关闭SnackBar
timeout当duration超时后,自动关闭SnackBar,本质上是调用 Scaffold.of(context).hideCurrentSnackBar()

查看其源码可以看出,关闭SnackBar的方式主要有二种:
1、通过调用 Scaffold.of(context).hideCurrentSnackBar()的方式
2、通过调用Scaffold.of(context).removeCurrentSnackBar()的方式

通过前文的讲解,我们知道SnackBar有一个 onVisible属性用来监听SnackBar已经在屏幕中显示出来,那么SnackBar关闭的时候是否也有回调呢?想要监听SnackBar的关闭,可以使用如下代码:

Scaffold.of(context).showSnackBar(SnackBar( ... )
).closed.then((SnackBarClosedReason reason) {
    println("SnackBar已经关闭”)
 });

在这里需要注意的是Scaffold.of(context).showSnackBar返回的是一个ScaffoldFeatureController对象,下面就来具体说说这个对象。

2、ScaffoldFeatureController源码简析

顾名思义,该类负责控制Scaffold组件的Feature(特征,特色),具体Feature指的是啥在这里先不做深究。在本文中Feature指的是SnackBar,事实上Scaffold.of(context).showSnackBar方法返回的就是一个ScaffoldFeatureController对象,通过这个对象我们可以做一些控制,比如上面所说的监听SnackBar的关闭时机。

其源码如下:

class ScaffoldFeatureController<T extends Widget, U> {
  const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
  ///在本文中指的是SnackBar
  final T _widget;
  //U类型在本文中指的是SnackBarClosedReason
  final Completer<U> _completer;

  /// 当snackBar等feature完全不可见的时候会回调该方法.
  Future<U> get closed => _completer.future;

  /// Remove the feature (e.g., bottom sheet or snack bar) from the scaffold.
  //回调函数,用来将SnackBar或者bottomSheet从scaffold里删除
  final VoidCallback close;

  ///该属性SnackBar没使用到,故此不多介绍.
  final StateSetter setState;
}

总结下来就是ScaffoldFeatureController持有了SnackBar,并且有一个Completer用来关闭SnackBar,比如hideCurrentSnackBar方法就是用了_completer,另外ScaffoldFeatureController还有一个closed的方法,给方法返回的是Completer内部的future。在这里简单看一下Completer的相关知识点:

// 创建一个一个Completer
var completer = Completer();
// 获取Completer内部的futer
var future = completer.future;
// 设置回调函数
future.then((value)=> print('$value'));
// 设置为完成状态
completer.complete("done");

completer在执行complete会自动把结果传给then方法。正如上文所说,我们可以通过下面代码来监听SanckBar的关闭:

//获取ScaffoldFeatureController 
ScaffoldFeatureController controller =  Scaffold.of(context).showSnackBar(SnackBar( ... ));
//获取ScaffoldFeatureController 的closed对象
Future future =controller .closed;
// 设置回调函数
future .then((SnackBarClosedReason reason) {
    println("SnackBar已经关闭”)
 });

分析到这里,不难猜出hideCurrentSnackBar内部其实就是获取到了当前SnackBar的completer 对象,然后执行其complete方法。其源码如下所示,验证了这个结论:

  void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) {
    //省略部分代码
    //从队列snackBars中获取第一个ScaffoldFeatureController,然后获取该对象的_completer
    final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
    if (mediaQuery.accessibleNavigation) {
      _snackBarController.value = 0.0;
      completer.complete(reason);
    } else {
    //动画
      _snackBarController.reverse().then<void>((void value) {
        if (!completer.isCompleted)
          completer.complete(reason);
      });
    }
      //省略部分代码
  }
showSnackBar详解

了解了showSnackBar的执行原理,SnackBar的原理也就是程序员头上的虱子了!

1、 _snackBarController简介

在上面分析hideCurrentSnackBar方法的时候,其内部又这么一段代码:

   AnimationController _snackBarController;
  ///hideCurrentSnackBar方法部分代码摘抄
   _snackBarController.reverse().then<void>((void value) {
        if (!completer.isCompleted)
          completer.complete(reason);
      });

_snackBarController是一个AnimationController ,可以用来对Animation进行控制。SnackBar在展示和关闭的时候都可以设置动画。所以在退出的时候要调用_snackBarController.reverse()方法,这样就使得跟进来的动画效果是反过来的(关于动画部分,点此了解更多)。

_snackBarController是Scaffold的一个属性,其初始化是在showSnackBar方法里面:

2、showSnackBar讲解
//队列,用来保存ScaffoldFeatureController
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
  ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) {
    //初始化_snackBarController 
    _snackBarController ??= SnackBar.createAnimationController(vsync: this)
    //添加动画状态监听
      ..addStatusListener(_handleSnackBarStatusChange);
      //如果SnackBar队列为空
    if (_snackBars.isEmpty) {
      //执行动画
      _snackBarController.forward();
    }
    
    //创建ScaffoldFeatureController
    ScaffoldFeatureController<SnackBar, SnackBarClosedReason> controller;
    controller = ScaffoldFeatureController<SnackBar, SnackBarClosedReason>._(
      //将_snackBarController设置给snackbar
      snackbar.withAnimation(_snackBarController, fallbackKey: UniqueKey()),
      Completer<SnackBarClosedReason>(),
      () {
        hideCurrentSnackBar(reason: SnackBarClosedReason.hide);
      },
      null, 
    );
    //调用setSate方法执行Scaffold的buid方法
    setState(() {
      //将ScaffoldFeatureController放在队列里面
      _snackBars.addLast(controller);
    });
    return controller;
  }

showSnackBar方法做了好多工作。总结下来有如下几条:
1、初始化_snackBarController这个AnimationController ,并设置动画状态监听。该监听代码如下:

  void _handleSnackBarStatusChange(AnimationStatus status) {
    switch (status) {
      case AnimationStatus.dismissed:///此时说明SnackBar已经消失
        setState(() {
         //从队列里删除SnackBar ,并重新调用Scaffold的build方法
          _snackBars.removeFirst();
        });
        if (_snackBars.isNotEmpty)//此时队列里还有SnackBar,继续显示下一条SnackBar
          _snackBarController.forward();
        break;
      case AnimationStatus.completed://动画执行完毕
        setState(() {
         //改变状态重新调用Scaffold的build方法
        });
        break;
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
        break;
    }
  }

从中上面代码可以看出,当SnackBar退出的时候,会将SnackBar所绑定的ScaffoldFeatureController,从_snackBars队列中删除。在删除的时候会调用setState方法,而后会自动调用build方法刷新页面,从而展示下一条SnackBar.

2、初始化ScaffoldFeatureController,将SnackBar交给ScaffoldFeatureController
3、将ScaffoldFeatureController放到_snackBars队列里。
4、然后调用Scaffold的setState方法,这样会重新调用Scaffold的build方法(这个方法最重要,也是SnackBar能展现的核心)
5、可以看出队列里的SnackBar共享了一个_snackBarController

Scaffold有一个队列_snackBars,该队列里装的是ScaffoldFeatureController。短时间内大量调用Scaffold.of(context).showSnackBar(SnackBar)方法,会把SnackBar封装成ScaffoldFeatureController对象,然后放到_snackBars队列里。
showSnackBar方法的末尾调用了 setState,此方法会重新调用Scaffold的build方法,所以来具体分析下build方法:

2、Scaffold的build方法讲解
 Widget build(BuildContext context) {

    //如果_snackBars队列不为空
    if (_snackBars.isNotEmpty) {
       //省略部分代码
       //从队列中取出第一个SnackBar
      final SnackBar snackBar = _snackBars.first._widget;
      //为snackBar设置一个定时器,计时结束后自动关闭SnackBar
      _snackBarTimer = Timer(snackBar.duration, () {
        //计时结束后自动关闭SnackBar
        hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
      });    
     //省略部分代码
    }
    final List<LayoutId> children = <LayoutId>[];
    bool isSnackBarFloating = false;
    //如果_snackBars队列不为空
    if (_snackBars.isNotEmpty) {
     //省略部分代码

     //主要是讲队列中的第一个SnackBar放到children数据里面
      _addIfNonNull(
        children,
        _snackBars.first._widget,
        //省略部分代码,
      );
    }
    return _ScaffoldScope(
    //省略部分代码
      child: PrimaryScrollController(
          child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget child) {
            return CustomMultiChildLayout(
              //使用children数据构建页面
              children: children,
               //省略部分代码,
            );
          }),
        ),
      ),
    );
  }
}

从上面代码不难看出,主要做了如下工作:
1、从队列里取出第一个SnackBar 对象,并且为该对象设置了定时器,定时器结束后就调用hideCurrentSnackBar方法自动关闭SnackBar
2、将队列里的第一个SnackBar通过_addIfNonNull方法,放入到children数组里面
3、将children数组交给PrimaryScrollController,从而完成Scaffold的创建。

到此为止,SnackBar的原理分析完毕,现在总结如下:
1、调用showSnackBar的时候,将SnackBar封装成ScaffoldFeatureController,放入队列里面
2、调用setState方法重绘页面,从队列中取出第一个SnackBar进行展示
3、当前SnackBar关闭后,将SnackBar对应的ScaffoldFeatureController从队列中删除,继续执行步骤2

SnackBar的目的主要是提供一个简单的提示性消息,交互能力和UI展示能力有限。如果想在底部展示更复杂的UI展现和交互能力,可以考虑使用Flutter 的BottomSheet组件

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郭梧悠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值