初次学习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作为contentbackgroundColorColor背景颜色,默认为ThemeData.snackBarTheme.backgroundColorelevationdoublez-coordinate 的值,类似于Card组件的elevation属性shapeShapeBorder可以设置Snackbar的形状,比如圆角矩形,上图是常规无圆角的矩形behaviorSnackBarBehavior为枚举类型,有两个值fixed和floatingactionSnackBarActionSnackBarAction是一个Widget类型,用来与用户交互的组件 ,其内部就是一个Text组件,配置该属性必须配置onPresseddurationDurationSnackBar的显示时长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() 关闭SnackBarremove通过执行Scaffold.of(context).removeCurrentSnackBar() 关闭SnackBartimeout当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 {
const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
///在本文中指的是SnackBar
final T _widget;
//U类型在本文中指的是SnackBarClosedReason
final Completer _completer;
/// 当snackBar等feature完全不可见的时候会回调该方法.
Future 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 completer = _snackBars.first._completer;
if (mediaQuery.accessibleNavigation) {
_snackBarController.value = 0.0;
completer.complete(reason);
} else {
//动画
_snackBarController.reverse().then((void value) {
if (!completer.isCompleted)
completer.complete(reason);
});
}
//省略部分代码
}
showSnackBar详解
了解了showSnackBar的执行原理,SnackBar的原理也就是程序员头上的虱子了!
1、 _snackBarController简介在上面分析hideCurrentSnackBar方法的时候,其内部又这么一段代码:
AnimationController _snackBarController;
///hideCurrentSnackBar方法部分代码摘抄
_snackBarController.reverse().then((void value) {
if (!completer.isCompleted)
completer.complete(reason);
});
_snackBarController是一个AnimationController ,可以用来对Animation进行控制。SnackBar在展示和关闭的时候都可以设置动画。所以在退出的时候要调用_snackBarController.reverse()方法,这样就使得跟进来的动画效果是反过来的(关于动画部分,点此了解更多)。
_snackBarController是Scaffold的一个属性,其初始化是在showSnackBar方法里面:
2、showSnackBar讲解//队列,用来保存ScaffoldFeatureController
final Queue _snackBars = Queue();
ScaffoldFeatureController showSnackBar(SnackBar snackbar) {
//初始化_snackBarController
_snackBarController ??= SnackBar.createAnimationController(vsync: this)
//添加动画状态监听
..addStatusListener(_handleSnackBarStatusChange);
//如果SnackBar队列为空
if (_snackBars.isEmpty) {
//执行动画
_snackBarController.forward();
}
//创建ScaffoldFeatureController
ScaffoldFeatureController controller;
controller = ScaffoldFeatureController._(
//将_snackBarController设置给snackbar
snackbar.withAnimation(_snackBarController, fallbackKey: UniqueKey()),
Completer(),
() {
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 children = [];
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组件