flutter 想要一款简洁美观的多端本地音乐播放器该怎么办?
本地音乐播放器在现在来说好像没什么人在用了吧,大家都直接使用某易,某Q来播放音乐,这些软件虽然很强大,但是不能播放本地保存的会员音乐,即使你本地有周杰伦的mp3音乐,在某Q播放器里面好像也不能播放,并且这些软件里面,丑陋的广告满天飞,让人头大。于是,我决定自己开发一款本地音乐播放器,界面要简洁、大气、美观,具备基本的本地音乐播放功能,一套代码实现跨平台多端运行。

渐变背景颜色实现
首先实现一个渐变色背景,使用Container组件的decoration属性,设置BoxDecoration来实现渐变色效果,在colors里面放上你喜欢的颜色。我实现的是比较粉嫩的背景颜色。
dart
Container(
alignment: Alignment.center,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
colors: [
Color.fromARGB(255, 252, 248, 228),
Color.fromARGB(255, 255, 221, 233),
],
),
),
),
底部导航按钮、切换页面
我们给播放器实现两个界面,第一个界面,也就是首页用来选择本地音乐存储路径,第二个界面展示播放界面。通过bottomNavigationBar组件实现底部导航功能栏,点击按钮切换界面。使用sizebox组件来控制显示区域大小,配合Row横向布局组件包裹IconButton按钮,点击按钮更改index页面索引,触发页面切换效果。
dart
bottomNavigationBar: SizedBox(
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: IconButton(
onPressed: () {
index.value = 0;
},
icon: const Icon(Icons.home)),
),
Expanded(
child: IconButton(
onPressed: () {
index.value = 1;
},
icon: const Icon(Icons.audiotrack)),
),
],
),
),
使用ValueListenableBuilder组件监听index页面索引,在点击按钮时index更改,触发pages显示页面重绘,实现点击切换页面效果。
dart
var pages = [
const MyApp1(),
const MyApp2(),
];
var index = ValueNotifier<int>(0);
ValueListenableBuilder(
builder: (BuildContext context, value, Widget? child) {
return pages[index.value];
},
valueListenable: index,
),

首页布局、本地音乐路径选取
主页放置一个按钮用来指定本地音乐文件路径,音乐文件路径选择我们使用file_picker插件,这个插件可以实现跨平台的文件选择功能,支持单个、多个文件选择,也可以指定路径,这个插件在前边的文章介绍过,大家有兴趣可以翻翻看看。
通过file_picker插件将指定的本地音乐文件路径存入musiclist中,当中播放列表,后边播放的时候要用到。只放置一个文件选择按钮有点单调,所以再放一段文字填充一下吧。
dart
List<String> musiclist = [""];
class MyApp1 extends StatelessWidget {
const MyApp1({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Music player",
style: TextStyle(fontSize: 50),
),
const Padding(padding: EdgeInsets.all(12)),
TextButton(
onPressed: () async {
FilePickerResult? result =
await FilePicker.platform.pickFiles(allowMultiple: true);
if (result != null) {
for (var i = 0; i < result.files.length; i++) {
debugPrint(result.files[i].path);
musiclist.add(result.files[i].path.toString());
}
} else {
// User canceled the picker
}
},
child: const Text("Choose Music Files"),
),
],
),
),
);
}
}

audioplayers插件
audioplayers插件可同时播放多个音频文件,适用于Android,iOS,Linux,macOS,Windows和Web,使用audioplayers插件可以实现音乐播放、暂停、停止、下一曲、上一曲等基本操作,也可以获取当前播放进度。播放的音乐文件可以是网络上资源、也可以是本地资源,很是方便。我们还可以监控播放进度和播放状态。
audioplayers插件初始化:AudioPlayer audioPlayer = AudioPlayer(); 音乐暂停:audioPlayer.pause(); 音乐停止:audioPlayer.stop(); 音乐播放:audioPlayer.play();
我们使用musiclist存储要播放的音乐路径,使用DeviceFileSource可以设置播放本地音乐文件,只需要指定音乐路径即可。
dart
AudioPlayer audioPlayer = AudioPlayer();
Future<void> pause() async {
audioPlayer.pause();
}
Future<void> stop() async {
audioPlayer.stop();
}
Future<void> playlocalfile() async {
audioPlayer.play(
DeviceFileSource(
musiclist[musicindex],
),
);
}
音乐播放界面布局
接着来实现播放页面。播放界面由上到下依次放置一个小汽车动画、音浪动画,这两个动画在音乐播放时会动起来,音乐暂停会停止播放。进度条也是必须具备的,随着音乐的播放指示进度。接着就是音乐曲目展示区域,显示正在播放的歌曲名。最后就是播放按键的实现了,具备基本的上一曲、下一曲、播放、暂停功能。
小汽车动画和波浪动画
动画部分我们依旧使用lottie来实现,前边的文章也有介绍过lottie动画,大家有兴趣可以翻翻我的主页。先去lottie主页找几个免费的、适合的动画下载下来,我找了一个小汽车动画、音浪动画、进度条动画,看起来还不错。
因为要控制动画的播放和暂停,所以会更改组件的状态,所以我们要用StatefulWidget有状态组件。在StatefulWidget有状态组件初始化时设置动画控制器topanimaController,后边都通过topanimaController来控制动画的播放。
dart
class TopAnimLottie extends StatefulWidget {
const TopAnimLottie({super.key});
@override
State<TopAnimLottie> createState() => _TopAnimLottieState();
}
class _TopAnimLottieState extends State<TopAnimLottie>
with TickerProviderStateMixin {
@override
void initState() {
super.initState();
topanimaController = AnimationController(vsync: this);
}
@override
void dispose() {
topanimaController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
child: Lottie.asset(
'assets/animation_lmdkfr6e.json',
width: 150,
height: 120,
controller: topanimaController,
onLoaded: (composition) {
setState(() {
topanimaController.duration = composition.duration;
});
},
),
);
}
}

进度条
因为进度条动画的播放要随着音乐进度更改,同时要能进能退,所以进度条动画与上边的小汽车动画在实现上略有不同,但依旧都是lottie动画。 在有状态组件中放置一个Timer.periodic定时器,每一秒计算一下播放进度,根据播放进度控制进度条动画的位置。使用audioPlayer.getCurrentPosition可以获得当前播放进度,使用audioPlayer.getDuration()可以获得全部播放时长。计算出播放进度后,利用proresscontroller.animateTo(left);控制进度条播放。
dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'audioplay.dart';
late AnimationController proresscontroller;
class Progressbar extends StatefulWidget {
const Progressbar({super.key});
@override
State<Progressbar> createState() => _ProgressbarState();
}
double left = 0;
class _ProgressbarState extends State<Progressbar>
with TickerProviderStateMixin {
@override
void initState() {
super.initState();
proresscontroller = AnimationController(vsync: this);
var timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
var cu = await audioPlayer.getCurrentPosition();
var all = await audioPlayer.getDuration();
var culist = cu.toString().split(":");
var alllist = all.toString().split(":");
var cud = double.parse(culist[1]) * 60 + double.parse(culist[2]);
var alld = double.parse(alllist[1]) * 60 + double.parse(alllist[2]);
left = cud / alld;
proresscontroller.animateTo(left);
});
}
@override
void dispose() {
proresscontroller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Lottie.asset(
'assets/91938-green-progress-bar-without-popover.json',
width: 200,
controller: proresscontroller,
onLoaded: (composition) {
setState(() {
proresscontroller.duration = composition.duration;
});
},
),
);
}
}

播放按键
播放按钮使用IconButton组件,在相应的上一曲、下一曲、播放、暂停按钮中实现对应的操作逻辑。点击上一曲或者下一曲按钮时,在前边定义的musiclist列表中切换要播放的音乐文件路径,实现歌曲切换。同时控制小汽车、音浪、进度条动画的播放状态。
播放或者暂停按钮使用flag来进行一个判断,根据播放状态来控制要展示的按钮图标,同时控制动画的播放和暂停。
dart
var flag = true;
class MyApp2 extends StatefulWidget {
const MyApp2({super.key});
@override
State<MyApp2> createState() => _MyApp2State();
}
class _MyApp2State extends State<MyApp2> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TopAnimLottie(),
DownAnimLottie(),
Padding(
padding: const EdgeInsets.fromLTRB(0, 12, 0, 8),
child: Progressbar(),
),
],
),
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
musiclist[musicindex]
.split("\\")[musiclist[musicindex].split("\\").length - 1],
style: const TextStyle(fontSize: 18),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () async {
if (musicindex > 0) {
musicindex = musicindex - 1;
}
flag = false;
await stop();
proresscontroller.reset();
playlocalfile();
downanimaController.reset();
downanimaController.repeat();
topanimaController.reset();
topanimaController.repeat();
setState(() {});
},
icon: const Icon(Icons.skip_previous),
),
Container(
child: flag
? IconButton(
onPressed: () {
flag = false;
setState(() {});
playlocalfile();
downanimaController.reset();
downanimaController.repeat();
topanimaController.reset();
topanimaController.repeat();
},
icon: const Icon(Icons.play_arrow))
: IconButton(
onPressed: () {
flag = true;
pause();
setState(() {});
topanimaController.stop();
downanimaController.stop();
},
icon: const Icon(Icons.pause)),
),
IconButton(
onPressed: () async {
if (musicindex + 1 < musiclist.length) {
musicindex = musicindex + 1;
}
flag = false;
proresscontroller.reset();
await stop();
playlocalfile();
downanimaController.reset();
downanimaController.repeat();
topanimaController.reset();
topanimaController.repeat();
setState(() {});
},
icon: const Icon(Icons.skip_next),
)
],
),
],
);
}
}

总结
以上就是我自己开发的音乐播放app啦,只是实现了本地的音乐播放功能,简单的加了一些动画,能满足最基本的播放音乐的需求,当然,还有很多功能没有实现,距离商业音乐播放器还有超级大的差距,后边如果有时间,再继续完善吧。大家可以多多提提建议。后边有时间把代码公开一下,有兴趣继续开发的小伙伴,我们可以一起合作开发哦。
