如何在Flutter的任何屏幕/页面上显示内容?
难度:中级
前言
最近我写了一些代码来处理WebSockets,我需要在任何屏幕/页面的顶部显示一个图标,以便在服务器发送通知时通知用户。
我尝试使用PopupRoute,showDialog ...但是永远无法获得我想要实现的目标。
当我们使用路由时,整个屏幕被覆盖,并且用户无法继续使用当前页面“ 正常 ” 工作,因为后者被另一个页面替换(覆盖)。因此,我继续我的调查,发现Overlay和OverlayEntry的概念。
通过阅读Flutter的源代码,我发现Flutter使用OverlayEntry来显示drag avatar(请参阅Draggable)。因此,我明白这就是我要找的东西。
覆盖
Flutter文档说:“ * overlay是一组可以独立管理的条目。Overlays让独立的widgets“浮动”在其他widgets之上......* ”。
用我自己很简单的话说。
overlay不外乎是一个layer (StatefulWidget),在所有widget之上,它包含一个Stack,这里我们可以添加任何小部件。
这些小部件称为OverlayEntry
OverlayState
Overlay是一个StatefulWidget。OverlayState是Overlay实例的State,负责渲染。
OverlayEntry
OverlayEntry是一个Widget,我们可以把他插入到Overlay。OverlayEntry一次最多只能插入到一个Overlay。
由于Overlay使用Stack布局,因此overlay窗口可以使用Positioned和AnimatedPositioned将自己定位在叠加层中。
嗯,这正是我需要的:能够在屏幕上的任何地方显示我的通知图标。
让我们直接跳转到一些代码
以下类在坐标(50.0,50.0)处显示一个Icon,并在2秒后将其删除。
import 'package:flutter/material.dart';
import 'dart:async';
class ShowNotificationIcon {
void show(BuildContext context) async {
OverlayState overlayState = Overlay.of(context);
OverlayEntry overlayEntry = new OverlayEntry(builder: _build);
overlayState.insert(overlayEntry);
await new Future.delayed(const Duration(seconds: 2));
overlayEntry.remove();
}
Widget _build(BuildContext context){
return new Positioned(
top: 50.0,
left: 50.0,
child: new Material(
color: Colors.transparent,
child: new Icon(Icons.warning, color: Colors.purple),
),
);
}
}
如您所见,为了能够显示图标,我们需要提供Context。为什么?
官方文档没有解释这一点,但是,看一下源代码,我们看到为每个Route创建了一个Overlay,因此它属于一个Context树(参见我关于上下文概念的文章)。因此,有必要找到对应于特定Context的OverlayState(第7行)。
就是这个! 从这个例子中,我们可以推导出任何类型的叠加内容和行为。
为了说明这一点,让我们构建一种闪烁的Toast,它在屏幕上的某个位置显示一个Widget,并在一定时间后消失。
示例:闪烁Toast
第一类是前一个例子的概括。它允许提供外部Widget构造函数。
import 'dart:async';
import 'package:flutter/material.dart';
class BlinkingToast {
bool _isVisible = false;
///
/// BuildContext context: the context from which we need to retrieve the Overlay
/// WidgetBuilder externalBuilder: (compulsory) external routine that builds the Widget to be displayed
/// Duration duration: (optional) duration after which the Widget will be removed
/// Offset position: (optional) position where you want to show the Widget
///
void show({
@required BuildContext context,
@required WidgetBuilder externalBuilder,
Duration duration = const Duration(seconds: 2),
Offset position = Offset.zero,
}) async {
// Prevent from showing multiple Widgets at the same time
if (_isVisible){
return;
}
_isVisible = true;
OverlayState overlayState = Overlay.of(context);
OverlayEntry overlayEntry = new OverlayEntry(
builder: (BuildContext context) => new BlinkingToastWidget(
widget: externalBuilder(context),
position: position,
),
);
overlayState.insert(overlayEntry);
await new Future.delayed(duration);
overlayEntry.remove();
_isVisible = false;
}
}
第二个类在屏幕上的某个位置显示Widget并使其闪烁。
有关动画概念的进一步说明,请参阅我关于此主题的文章。
class BlinkingToastWidget extends StatefulWidget {
BlinkingToastWidget({
Key key,
@required this.widget,
@required this.position,
}): super(key: key);
final Widget widget;
final Offset position;
@override
_BlinkingToastWidgetState createState() => new _BlinkingToastWidgetState();
}
class _BlinkingToastWidgetState extends State
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _animation;
@override
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animation = new Tween(begin: 0.0, end: 1.0).animate(new CurvedAnimation(
parent: _controller,
curve: new Interval(0.0, 0.5)
))
..addListener(() {
if (mounted){
setState(() {
// Refresh
});
}
})
..addStatusListener((AnimationStatus status){
if (status == AnimationStatus.completed){
_controller.reverse().orCancel;
} else if (status == AnimationStatus.dismissed){
_controller.forward().orCancel;
}
});
_controller.forward().orCancel;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return new Positioned(
top: widget.position.dy,
left: widget.position.dx,
child: new IgnorePointer(
child: new Material(
color: Colors.transparent,
child: new Opacity(
opacity: _animation.value,
child: widget.widget,
),
),
));
}
}
要调用此BlinkingToast:
BlinkingToast toast = new BlinkingToast();
toast.show(
context: context,
externalBuilder: (BuildContext context){
return new Icon(Icons.warning, color: Colors.purple);
},
duration: new Duration(seconds: 5),
position: new Offset(50.0, 50.0),
);
结论
这是一篇非常简短的文章,仅旨在分享在任何屏幕上显示Widget的方式。