初次学习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可以进行如下配置:

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

下面就根据上面的属性,配置了一个圆角红色背景,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);///在本文中指的是SnackBarfinal T _widget;//U类型在本文中指的是SnackBarClosedReasonfinal 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,然后获取该对象的_completerfinal 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();}//创建ScaffoldFeatureControllerScaffoldFeatureController<SnackBar, SnackBarClosedReason> controller;controller = ScaffoldFeatureController<SnackBar, SnackBarClosedReason>._(//将_snackBarController设置给snackbarsnackbar.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) {//省略部分代码//从队列中取出第一个SnackBarfinal SnackBar snackBar = _snackBars.first._widget;//为snackBar设置一个定时器,计时结束后自动关闭SnackBar_snackBarTimer = Timer(snackBar.duration, () {//计时结束后自动关闭SnackBarhideCurrentSnackBar(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组件

Flutter之SnackBar原理详解相关推荐

  1. Flutter完整开发实战详解(十七、 实用技巧与填坑二)

    作为系列文章的第十七篇,本篇再一次带来 Flutter 开发过程中的实用技巧,让你继续弯道超车,全篇均为个人的日常干货总结,以实用填坑为主,让你少走弯路狂飙车. Flutter 完整实战实战系列文章专 ...

  2. CRF(条件随机场)与Viterbi(维特比)算法原理详解

    摘自:https://mp.weixin.qq.com/s/GXbFxlExDtjtQe-OPwfokA https://www.cnblogs.com/zhibei/p/9391014.html C ...

  3. LVS原理详解(3种工作方式8种调度算法)--老男孩

    一.LVS原理详解(4种工作方式8种调度算法) 集群简介 集群就是一组独立的计算机,协同工作,对外提供服务.对客户端来说像是一台服务器提供服务. LVS在企业架构中的位置: 以上的架构只是众多企业里面 ...

  4. jQuery中getJSON跨域原理详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp28 jQuery中getJSON跨域原理详解 前几天我再开发一个叫 河蟹工 ...

  5. nginx配置文件及工作原理详解

    nginx配置文件及工作原理详解 1 nginx配置文件的结构 2 nginx工作原理 1 nginx配置文件的结构 1)以下是nginx配置文件默认的主要内容: #user nobody; #配置用 ...

  6. EMD算法之Hilbert-Huang Transform原理详解和案例分析

    目录 Hilbert-Huang Transform 希尔伯特-黄变换 Section I 人物简介 Section II Hilbert-Huang的应用领域 Section III Hilbert ...

  7. 图像质量损失函数SSIM Loss的原理详解和代码具体实现

    本文转自微信公众号SIGAI 文章PDF见: http://www.tensorinfinity.com/paper_164.html http://www.360doc.com/content/19 ...

  8. 深入剖析Redis系列(三) - Redis集群模式搭建与原理详解

    前言 在 Redis 3.0 之前,使用 哨兵(sentinel)机制来监控各个节点之间的状态.Redis Cluster 是 Redis 的 分布式解决方案,在 3.0 版本正式推出,有效地解决了 ...

  9. 【Android架构师java原理详解】二;反射原理及动态代理模式

    前言: 本篇为Android架构师java原理专题二:反射原理及动态代理模式 大公司面试都要求我们有扎实的Java语言基础.而很多Android开发朋友这一块并不是很熟练,甚至半路初级底子很薄,这给我 ...

  10. SVM分类器原理详解

    SVM分类器原理详解 标签: svm文本分类java 2015-08-21 11:51 2399人阅读 评论(0) 收藏 举报  分类: 数据挖掘 文本处理(16)  机器学习 分类算法(10)  目 ...

最新文章

  1. java中基本字节输出流类是_java中基本输入输出流的解释
  2. 原理+代码实战 | 双目视觉中的极线校正
  3. [OpenGL]未来视觉1-Android摄像头采集基础
  4. 加锁查询 FOR UPDATE 解决表格查询极慢的问题
  5. 直接拿来用!最火的iOS开源项目(二)
  6. android udp 收发例子_如何利用光衰减器来测试光纤收发器的灵敏度?
  7. python【力扣LeetCode算法题库】27-移除元素
  8. iPhonexr安兔兔html5测试,给大家科普下iphonexr苹果手机安兔兔跑分多少分
  9. 别致的上传思路导致getshell的案例
  10. linq to sql报错,
  11. 清除zencart分类页多页后面的disp_order sort字符串的方法
  12. CUDA C/C++ 教程一:加速应用程序
  13. mac php71 php fpm,Mac PHP-fpm
  14. 常用的电脑显示器接口有哪几种?
  15. 四象限分析法分析你是否适合做管理
  16. select苹果手机样式设置
  17. python之路(1)_重要函数使用
  18. Excel 分组统计不重复项
  19. 统计本段话的高频词汇——报错:KeyError
  20. JavaScript note

热门文章

  1. 前端入职后很痛苦_NGW前端新技术赛场:Serverless SSR 技术内幕
  2. Node.js:Node模块简介
  3. Java中的几种设计模式:行为型模式
  4. SLAM_SLAM中一般是如何求解相机的运动的?
  5. 1024 科学计数法
  6. 机器学习课程笔记【五】- 支持向量机(2)
  7. 线性代数【五】向量(2):向量组的秩,向量内积、正交,正交规范化,向量空间
  8. LIO-SAM探秘之文章索引
  9. GSL数学库解多参数方程
  10. 第 2 讲 初识 SLAM