系列指路:
Flutter自绘组件:微信悬浮窗(一)
Flutter自绘组件:微信悬浮窗(二)
Flutter自绘组件:微信悬浮窗(三)
Flutter自绘组件:微信悬浮窗(四)
悬浮窗最终的实现效果如下:
实现思路
我们在上一篇文章中已经完整实现了悬浮窗的完整代码,现在需要的只是将FloatingWindow
类套上一层OverlayEntry
的浮层就可以完结撒花了。我们先来讲讲OverlayEntry
是什么。
OverlayEntry
OverlayEntry
可以理解为一个浮层元素,我们常见的MaterialApp
创建时,会创建一个Overlay
浮层集合,然后利用这个 Navigator
来管理路由中的界面。
由于百度了一下,没有百度到详解,对于这种情况,我们也只能从头自己进行详解,即读源码注释。
我们先来读一下Overlay
的源码注释:
/// A [Stack] of entries that can be managed independently.
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
///
/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
/// navigator uses its overlay to manage the visual appearance of its routes.
///
/// See also:
///
/// * [OverlayEntry].
/// * [OverlayState].
/// * [WidgetsApp].
/// * [MaterialApp].
class Overlay extends StatefulWidget
翻译过来的大致意思使,Overlay
可以通过把独立的子Widget
们插入到Overlay
的Stack
中来让他们“漂浮”在顶层Widget
们其他可见元素之上,而且这些独立的子Widget
们通过OverlayEntry
来管理自身是否参与到Overlay
中。
我们在提到MaterialApp
已经存在了一个Overlay
,我们只需要通过Overlay.of
即可获得当前MaterialApp
的OverlayState
对象。我们可以看一下这个方法的源码:
/// The state from the closest instance of this class that encloses the given context.
///
/// In debug mode, if the `debugRequiredFor` argument is provided then this
/// function will assert that an overlay was found and will throw an exception
/// if not. The exception attempts to explain that the calling [Widget] (the
/// one given by the `debugRequiredFor` argument) needs an [Overlay] to be
/// present to function.
///
/// Typical usage is as follows:
///
/// ```dart
/// OverlayState overlay = Overlay.of(context);
/// ```
///
/// If `rootOverlay` is set to true, the state from the furthest instance of
/// this class is given instead. Useful for installing overlay entries
/// above all subsequent instances of [Overlay].
static OverlayState of(
BuildContext context, {
bool rootOverlay = false,
Widget debugRequiredFor,
})
可以看到rooteOverlay
会获取到最遥远的Overlay
实例的状态,如果我们需要把OverlayEntry
置于后来所有的overlay
之上的话是十分有用的。我们的悬浮窗即使在切换页面,无论何种情况下都应置于最高层,因此,这个参数应该设为true
。
可能需要注意的逻辑就在于,当悬浮窗的列表项全部关闭时,再进行添加则需要移除原先的浮层,然后再重新申请浮层资源。如果当前浮层还存在则不需要这么做,我们如何来判断悬浮窗的列表项是否已经全部关闭呢?我们在实现FloatingWindow
的时候定义了一个isEmpty
变量来判断列表是否为空,我们只需要把共享数据windowModel
设为静态数据,然后再在FloatingWidget
中定义一个isEmpty
的方法就可以得知当前的悬浮窗列表项是否为空。而往悬浮窗中添加列表项的实现也很简单,只需要把FloatingWindowState
中代表列表项的数据列表ls
设为静态数据,再在FloatingWidget
中添加静态方法add
用于向ls
中添加列表项数据,FloatingWindow
修改后的代码如下:
/// [FloatingWindow] 悬浮窗
class FloatingWindow extends StatefulWidget {
@override
_FloatingWindowState createState() => _FloatingWindowState();
/// 添加列表项数据
static void add(Map<String,String> element){
_FloatingWindowState.ls.add(element);
}
/// 判断列表项是否为空
static bool isEmpty(){
return _FloatingWindowState.windowModel.isEmpty;
}
}
class _FloatingWindowState extends State<FloatingWindow> {
static List<Map<String,String>> ls = [];
/// 悬浮窗共享数据
static FloatingWindowModel windowModel;
/// [isEntering] 列表项是否拥有进场动画
bool isEntering = true;
@override
void initState() {
// TODO: implement initState
super.initState();
windowModel = new FloatingWindowModel(dataList: ls,isLeft: true);
isEntering = true;
}
@override
Widget build(BuildContext context) {
return FloatingWindowSharedDataWidget(
data: windowModel,
child: windowModel.isEmpty ? Container() : Stack(
fit: StackFit.expand,
children: [
/// 列表项遮盖层,增加淡化切换动画
AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: windowModel.isButton ? Container() : GestureDetector(
onTap: (){
FloatingItem.reverse();
Future.delayed(Duration(milliseconds: 110),(){
setState(() {
windowModel.isButton = true;
windowModel.itemTop = -1.0;
});
});
},
child: Container(
decoration: BoxDecoration(color: Color.fromRGBO(0xEF, 0xEF, 0xEF, 0.9)),
),
),
),
NotificationListener<ClickNotification>(
onNotification: (notification){
/// 列表项关闭事件
if(notification.deletedIndex != -1){
windowModel.deleteIndex = notification.deletedIndex;
setState(() {
FloatingItem.resetList();
windowModel.dataList.removeAt(notification.deletedIndex);
isEntering = false;
});
}
/// 列表点击事件
if(notification.clickIndex != -1){
print(notification.clickIndex);
}
/// 悬浮按钮点击Widget改变事件
if(notification.changeWidget){
setState(() {
/// 释放列表进出场动画资源
FloatingItem.resetList();
windowModel.isButton = false;
isEntering = true;
});
}
return false;
},
child: windowModel.isButton ? FloatingButton():FloatingItems(isEntering: isEntering,),
)
],
),
);
}
}
FloatingWindowOverEntry
代码实现如下:
import 'package:flutter/material.dart';
import 'FloatingWindow.dart';
/// [FloatingWindowOverlayEntry] 悬浮窗浮层显示
class FloatingWindowOverlayEntry{
/// [_overlayEntry] 悬浮窗浮层
static OverlayEntry _overlayEntry;
/// [_overlayEntry] 悬浮窗
static FloatingWindow _floatingWindow;
/// 添加条项
static void add(BuildContext context,{Map<String,String> element}){
/// 如果没有浮层则初始化
if(_overlayEntry == null){
_floatingWindow = new FloatingWindow();
_overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return _floatingWindow;
}
);
Overlay.of(context,rootOverlay: true).insert(_overlayEntry);
}
/// 存在浮层
else{
/// 如果列表项为空,则清除原先浮层,然后新建浮层插入。
if(FloatingWindow.isEmpty()){
/// 清除原先浮层
_overlayEntry.remove();
_floatingWindow = new FloatingWindow();
/// 新建浮层
_overlayEntry = OverlayEntry(
builder: (BuildContext context){
return _floatingWindow;
}
);
/// 插入浮层
Overlay.of(context,rootOverlay: true).insert(_overlayEntry);
}
}
/// 添加列表项数据
FloatingWindow.add(element);
/// 标记脏值刷新
_overlayEntry.markNeedsBuild();
}
}
使用方法也十分简单:
FloatingWindowOverlayEntry.add(context,element: {'title': "微信悬浮窗","imageUrl":"assets/Images/vnote.png"}),
main.dart调用代码
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home:new Scaffold(
body: Stack(
children: [
/// 用于测试遮盖层是否生效
Positioned(
left: 250,
top: 250,
child: Container(width: 50,height: 100,color: Colors.red,),
),
Positioned(
left: 250,
top: 50,
child:TestWidget()
),
],
),
),
);
}
}
class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
RaisedButton(
child: Text('show'),
),
RaisedButton(
onPressed: () => FloatingWindowOverlayEntry.add(context,element: {'title': "微信悬浮窗","imageUrl":"assets/Images/vnote.png"}),
child: Text('add'),
)
],
);
}
}
总结
完结撒花,可以安心写其他系列,迟点可能会把这个项目开源到gitee
上。接下来更新drogon
系列/uni-app
系列以及其他项目实战记录。