前言
网易云音乐ios端和android端底部的音乐栏几乎都覆盖了所有页面,但是还是有些许不同:
从上图可以看出ios端的音乐栏在页面切换时总是固定的,而android端则有一个较为明显的路由切换动画。
在编程开始之前,先来了解一下在Android原生中是如何实现的,知乎文章:www.zhihu.com/question/36...
从最高赞回答中可以知晓,在Android原生中实现固定音乐栏的方法,就是在每个activity都写一个音乐栏...
呃...这种方法不说繁琐,简直就是麻烦。没有批评网易大佬的意思,也许他们有更多的考量。
那么如果在flutter实现固定音乐栏的话,有没有更简单的方法呢。
悬浮窗
的确,如果创建一个固定位置的悬浮窗的话,实现出来的效果应该会和ios端更贴近,即页面跳转时无路由动画。
那么应该怎么实现悬浮窗呢?
最简单的方法就是OverlayEntry了。
一、封装一个OverlayEntry
音乐栏全局有且只有一个,因此使用工厂模式创建单例类:
scss
class MusicBar {
static final MusicBar _instance = MusicBar._();
factory MusicBar() => _instance;
MusicBar._();
///显示音乐栏
OverlayEntry? overlayEntry;
show(BuildContext context,Widget child,{double? top,double? left}) {
if (overlayEntry == null) {
overlayEntry = OverlayEntry(builder: (BuildContext context) {
return MusicBarWidget(
top: top??100,
left: left??100,
child: child,
);
});
Overlay.of(context)?.insert(overlayEntry!);
}
}
///隐藏音乐栏
void hide() {
overlayEntry?.remove();
overlayEntry = null;
}
}
二、封装悬浮窗Widget
网易云音乐首页音乐栏下面还有个导航栏,当路由至其他界面时导航栏消失,这时音乐栏的下方就会空出一片地方。参考ios的处理方法,我添加了一段下沉动画,而回到首页时再上浮回来,同样用eventbus广播。
ini
class _MusicBarWidgetState extends State<MusicBarWidget>
with TickerProviderStateMixin {
AnimationController? _controller;
double left = 0;
double top = 0;
double maxX = 0;
double maxY = 0;
var parentKey = GlobalKey();
var childKey = GlobalKey();
var parentSize = const Size(0, 0);
var childSize = const Size(0, 0);
late StreamSubscription _subscription;
@override
void initState() {
_subscription = eventBus.on().listen((event) {
EventObj eventObj = event;
if (eventObj.code == EventCode.toBottom) {
toBottom();
}
if (eventObj.code == EventCode.riseUp) {
riseUp();
}
});
left = widget.left;
top = widget.top;
WidgetsBinding.instance.addPostFrameCallback((d) {
parentSize = getWidgetSize(parentKey);
childSize = getWidgetSize(childKey);
maxX = parentSize.width - childSize.width;
maxY = parentSize.height - childSize.height;
});
super.initState();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
key: parentKey,
fit: StackFit.expand,
children: [
Positioned(
key: childKey,
left: left,
top: top,
child: widget.child,
)
],
);
}
//底部导航栏消失时沉底
void toBottom() {
_controller = AnimationController(vsync: this)..duration = widget.duration;
var animation = Tween<double>(begin: top, end: maxY).animate(_controller!);
animation.addListener(() {
top = animation.value;
setState(() {});
});
_controller!.forward();
}
//底部导航栏出现时抬高
void riseUp() {
_controller = AnimationController(vsync: this)..duration = widget.duration;
var animation =
Tween<double>(begin: top, end: maxY - 56).animate(_controller!);
animation.addListener(() {
top = animation.value;
setState(() {});
});
_controller!.forward();
}
Size getWidgetSize(GlobalKey key) {
final RenderBox renderBox =
key.currentContext?.findRenderObject() as RenderBox;
return renderBox.size;
}
}
三、使用方法
scss
MusicBar smallWindowManager = MusicBar();
smallWindowManager.show(
context,
_musicBar(size,smallWindowManager,context),top: size.height-ScreenUtil().setWidth(70)-56,left: 0);
//_musicBar()为Widget,样式自己编写
虽然这样子实现的悬浮窗不受路由影响,但是在某些页面需要隐藏的情况下也需要注意显示隐藏的逻辑,在特定的生命周期进行显示隐藏。
最后来张效果图