今天突然有个想听的歌木有版权了,久违的打开了QQ音乐,不仅找到了想听的歌,还被播放页的智能光效功能种草了,那就用Flutter来模仿一下吧
效果分析
QQ音乐智能光效实机效果如下图底部所示,可见背景色逐渐变化位置与形状,给播放器增加了一种动态感。

那么实现如上效果,首先需要Flutter的stack widget给当前视图设置一个背景图层,然后在背景图层中画出光效轮廓,最后使用animation赋予光效动态效果,分析完了就开始动手
背景光效实现
一开始想法是制作一张光效效果的图片,通过stack分层将其置于背景图层实现该效果。后面转而一想能不能使用Flutter绘出类似效果呢,答案当然是可以的,方法就是使用Gradient类。下面简要介绍一下该类的三种绘制效果,感兴趣的朋友可以复制粘贴至DartPad快速预览效果。
Gradient.linear
线性渐变色,前端较为常用的效果,用来实现线性变化的颜色效果。在Flutter中使用同样简单方便,通过设置渐变色的起点位置begin与终点位置end,及每个位置对应的颜色即可实现线性渐变色,下图为由左上至右下的背景颜色渐变。
如下为实现代码,可直接在Dartpad查看效果
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Gradient',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Gradient'),
),
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purpleAccent],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
),
);
}
}
Gradient.radial
与上文线性渐变色不同,轮廓渐变色是从中心一点向外辐射,效果如下。

代码实现与线性不同的是需要定义轮廓的中心位置,也就是开始辐射的位置。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Gradient',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Gradient'),
),
body: Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
colors: [Colors.blue, Colors.purpleAccent],
center: Alignment(0.25, 0.25),
),
),
),
),
);
}
}
Gradient.sweep
gradient sweep是根据用户给定的起始角度与终止角度进行渐变色的绘制,效果如下。

代码实现如下,主要变化的参数是渐变色的起始角度与终止角度。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Gradient',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Gradient'),
),
body: Container(
decoration: const BoxDecoration(
gradient: SweepGradient(
colors: [Colors.blue, Colors.purpleAccent],
startAngle: 0.0,
endAngle: 3.0,
),
),
),
),
);
}
}
音乐播放器静止光效实现
测试过Gradient三种渐变效果,自然可以发现,我们首先可以利用RadialGradient的效果来实现简易的光效效果。这里使用从前写的音乐播放器的小demo来测试效果,根据QQ播放器的实机效果需要将渐变光晕中心点置于页面底部的可见区域之外,效果如下。

通过stack分层添加Gradient渐变色图层,代码如下。
child: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
// colors: [Color(0xFF3F3F3F), Color(0xFF181818)],
colors: [Colors.greenAccent[200]!, Colors.grey[200]!],
center: Alignment(0.4, 0.9),
stops: [0.2, 1.0]),
),
),
Container(
child: ListView.builder(
padding: const EdgeInsets.all(4),
itemCount: widget.tracks.length,
itemExtent: 54.0,
itemBuilder: (context, index) {
return ListTile(
title: Text(
widget.tracks[index].name,
style: TextStyle(color: Colors.black54),
),
subtitle: Text(
widget.tracks[index].artists[0].name,
style: TextStyle(
color: Colors.grey[400],
),
),
onTap: () {
_trackTapped(index);
},
);
},
),
),
],
),
动态效果实现
既然静态的光晕效果已经实现了,那下一步就是如何让光晕动起来~,说到动起来那自然是通过Flutter的动画效果实现。 观看qq音乐的光效图发现其光晕的大小,位置和形状都是随时间而变化的,那么我们也给静态gradient设置位置与大小的动态值,看看动画效果如何。
动态位置
首先试着动态改变光晕位置,更改原始类为Tween动画类,增加对initState以及dispose的处理。
class _DetailPageState extends State<DetailPage>
with SingleTickerProviderStateMixin { // 增加mixin
// 增加动画相关参数
late Animation animation;
late AnimationController controller;
@override
void initState() {
super.initState();
// 控制动画时长为8s,循环播放
controller =
AnimationController(duration: const Duration(seconds: 8), vsync: this)
..repeat(reverse: true);
// 控制水平位置从左至右
animation = Tween<double>(begin: -0.8, end: 0.8).animate(controller)
..addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
// colors: [Color(0xFF3F3F3F), Color(0xFF181818)],
colors: [Colors.greenAccent[200]!, Colors.grey[200]!],
center: Alignment(animation.value, 0.9),
stops: [0.2, sizeAnimation.value]),
),
),
Container(
child: ListView.builder(
padding: const EdgeInsets.all(4),
itemCount: widget.tracks.length,
itemExtent: 54.0,
itemBuilder: (context, index) {
return ListTile(
title: Text(
widget.tracks[index].name,
style: TextStyle(color: Colors.black54),
),
subtitle: Text(
widget.tracks[index].artists[0].name,
style: TextStyle(
color: Colors.grey[500],
),
),
onTap: () {
_trackTapped(index);
},
);
},
),
),
],
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
经测试执行正确,那接下来再增加一个变换大小动画,真正达到模仿动态光晕的效果。
动态大小
观察QQ音乐的光晕大小发现其是不断变化的,而不是规则的从小变大,这就意味着上文使用的Tween动画无法支持该类变化。查询官方得知可以使用tween sequence动画,提前赋给动画一个不规则的大小序列值,通过tween sequence展现对应的不规则大小动画,代码如下。
// 增加动画
late Animation sizeAnimation;
// 增加序列值
Tween<double>(begin: 0.4, end: 1.0)
@override
void initState() {
super.initState();
final sequenceItems = <TweenSequenceItem<double?>>[];
for (var i = 0; i < sizes.length; i++) {
final weight = 1 / sizes.length;
sequenceItems.add(
TweenSequenceItem<double?>(
tween: Tween<double>(begin: 0.4, end: sizes[i]),
weight: weight,
),
);
}
controller =
AnimationController(duration: const Duration(seconds: 8), vsync: this)
..repeat(reverse: true);
locationAnimation = Tween<double>(begin: -0.8, end: 0.8).animate(controller)
..addListener(() {
setState(() {});
});
// 增加大小动画
sizeAnimation = TweenSequence(sequenceItems).animate(controller)
..addListener(() {
setState(() {});
});
}
效果如下

可见背景光晕随时间动态变化位置与大小,基本功能已经实现,剩下的就是颜色,动画时长,大小等细节优化了。
结语
本文通过Grdient与动画实现了对于QQ音乐智能光效功能的模拟,整体做下来主要是对于gradient类的熟悉和动画相关类的实现,难度不是特别高,但是美观度还需大小,颜色等还需细节调整,可见Flutter作为跨平台框架,在UI上提供的自定义功能还是十分完善的。 本文demo以上传Github,感兴趣的同学可以star一下。