最近加了个移动互联实验室,学了flutter后打算自己做一个app练练手,了解到了网易云音乐接口便决定做一个简单的云音乐APP(仅供学习)

一、项目框架搭建

1、搭建基础的项目框架


介绍一个每个文件夹的用途
animation : 存放自定义动画
config : 存放配置文件(如电影、音乐接口地址等静态资源)
data : 存放本地数据文件
model : 网络元素model
pages : app页面
utils : 工具类
widget : 自定义组件
main.dart : app入口文件

配置本地文件夹assets,之后在登录页会用到文件夹中的两张图片


需要的接口地址
constant.dart

  /// 接口// 音乐热歌榜接口地址  :  https://music.163.com/api/playlist/detail?id=3778678static const String musicApiUrl_host = "music.163.com";static const String musicApiUrl_path = "/api/playlist/detail";// 音乐搜索  http://musicapi.leanapp.cn/search?keywords=static const String musicSearchUrl_host = "musicapi.leanapp.cn";static const String musicSearchUrl_path = "/search";// 音乐评论  http://musicapi.leanapp.cn/comment/music?id=27588968&limit=1static const String musicCommentUrl_host = "musicapi.leanapp.cn";static const String musicCommentUrl_path = "/comment/music";// 音乐播放地址 https://api.imjad.cn/cloudmusic/?type=song&id=112878&br=128000// 音乐歌词    https://api.imjad.cn/cloudmusic/?type=lyric&id=112878&br=128000static const String musicPlayLyricUrl_host = "api.imjad.cn";static const String musicPlayLyricUrl_path = "/cloudmusic";// 个人歌单// http://music.163.com/api/user/playlist/?offset=0&limit=100&uid=1927677638static const String personalPlayListApiUrl_host = "music.163.com";static const String personalPlayListApiUrl_path = "/api/user/playlist";// 个人信息// https://music.163.com/api/v1/user/detail/1927677638static const String personalInfoUrl_host = "music.163.com";static const String personalInfoUrl_path = "/api/v1/user/detail/";// 歌单详情 https://music.163.com/api/playlist/detail?id=24381616static const String playlistDetailUrl_host = "music.163.com";static const String playlistDetailUrl_path = "/api/playlist/detail";// 歌单评论 http://musicapi.leanapp.cn/comment/playlist?id=1static const String playlistCommentUrl_host = "musicapi.leanapp.cn";static const String playlistCommentUrl_path = "/comment/playlist";// 精品歌单 http://musicapi.leanapp.cn/top/playlist/highquality/华语static const String playlistHighQualityUrl_host = "musicapi.leanapp.cn";static const String playlistHighQualityUrl_path = "/top/playlist/highquality";// 相似歌单  http://musicapi.leanapp.cn/simi/playlist?id=347230static const String playlistSimiUrl_host = "musicapi.leanapp.cn";static const String playlistSimiUrl_path = "/simi/playlist";// 歌手榜单  http://music.163.com/api/artist/list   http://musicapi.leanapp.cn/artist/liststatic const String singerRankUrl_host = "musicapi.leanapp.cn";static const String singerRankUrl_path = "/artist/list";// 歌手热门歌曲 http://music.163.com/api/artist/5781  歌手信息和热门歌曲static const String singerTopMusicUrl_host = "music.163.com";static const String singerTopMusicUrl_path = "/api/artist/";// 歌手专辑列表 http://music.163.com/api/artist/albums/3684  歌手id  http://musicapi.leanapp.cn/artist/album?id=6452&limit=30static const String singerAlbumUrl_host = "music.163.com";static const String singerAlbumUrl_path = "/api/artist/albums/";// 专辑详情  https://music.163.com/api/album/90743831   专辑idstatic const String albumDetailUrl_host = "music.163.com";static const String albumDetailUrl_path = "/api/album/";// 歌手描述 http://musicapi.leanapp.cn/artist/desc?id=6452static const String singerDescUrl_host = "musicapi.leanapp.cn";static const String singerDescUrl_path = "/artist/desc";// 歌曲MV  http://music.163.com/api/mv/detail?id=319104&type=mp4

2、构建登录页

登录页基本实现:
a)、云音乐logo
b)、"云音乐"文本
c)、一个用户名输入框和一个密码输入框,账号密码未输入情况下,“登录"按钮不可点击。当输入账号密码时,登录按钮变为白色且可点击
d)、密码输入框加入"密码是否可见按钮”(小眼睛按钮)支持点击切换密码是否可见(图片资源在第一步已经导入)
e)、点击登录按钮,校验与已经设定的密码是否相符,并弹出密码正确与否提示
f)、登录页面使用“轻量级数据存储”形式(SharedPreferences)存储输入的账户和密码,下次打开时自动载入账户和密码且允许点击登录。

由于登录页实现比较简单,这里就不过多解释了,直接放上完成图


登录后禁止返回

// Navigator.push(context, MaterialPageRoute(//   builder: (context) => Tabs())// );// 禁止返回上个页面Navigator.of(context).pushAndRemoveUntil(new MaterialPageRoute(builder: (context) => Tabs()), (route) => route == null);

3、Tab页

登录成功后跳转到tab页
该tab页包括4个页面:歌单广场、歌手排行榜、音乐热歌榜,个人中心页。在flutter中通过BottomNavigationBar创建底部tab选项

Tabs代码

import 'package:flutter/material.dart';
import 'package:flutter_app_realtimeinfo/pages/music_page.dart';
import 'package:flutter_app_realtimeinfo/pages/personal_page.dart';
import 'package:flutter_app_realtimeinfo/pages/playlist_page.dart';
import 'package:flutter_app_realtimeinfo/pages/singer_page.dart';class Tabs extends StatefulWidget {@override_TabsState createState() => _TabsState();
}class _TabsState extends State<Tabs> {int _currentIndex = 0;final List<Widget> _pageList = [PlaylistPage(),// 歌单广场页SingerPage(),// 歌手榜单页MusicPage(),// 热歌榜PersonalPage()// 个人中心];final List<BottomNavigationBarItem> bottomNavigationBarItems = [BottomNavigationBarItem(icon: Icon(Icons.featured_play_list),title: Text('歌单')),BottomNavigationBarItem(icon: Icon(Icons.mic_none_outlined),title: Text('歌手')),BottomNavigationBarItem(icon: Icon(Icons.music_note_outlined),title: Text('热歌榜')),BottomNavigationBarItem(icon: Icon(Icons.person),title: Text('我的')),];@overridevoid initState(){super.initState();}@overrideWidget build(BuildContext context) {return Scaffold(bottomNavigationBar: BottomNavigationBar(type: BottomNavigationBarType.fixed,// BottomNavigationBar 超过3个之后 添加currentIndex: this._currentIndex,onTap: (int index){setState((){_currentIndex = index;});},items: this.bottomNavigationBarItems,),body: IndexedStack(index: _currentIndex,children: _pageList,),);}
}

4、歌单广场页

歌单广场页面的构造十分简单,分成两个部分:展示6条精品歌单的轮播图、精品歌单的网格布局。点击歌单后通过歌单id获取歌单数据并跳转到歌单详情页。

这个页面需要注意的是页面保持的方法。

先介绍一下页面保持是什么。一般情况下,我们使用tab切换的时候希望操作完毕之后,能够记住上个页面的状态,但是使用Flutter的BottomNavigationBar的时候默认是不记录页面状态的,即切换页面会导致重新加载。这对我们来说很痛苦,而且非常的浪费资源。

如果要想我们的页面在切换完毕之后记录之前的状态。需要一下几个步骤:

1、在包含BottomNavigationBar的页面中,body应该返回IndexedStack或者Pageview

2、想要保持状态的页面必须是StatefullWidget,并且在相应的页面的state中混入AutomaticKeepAliveClientMixin类重写wantKeepAlive方法并返回true


完成图:

5、歌单详情页

电影详情页基本实现:

a)、歌单封面、歌单名称、歌单描述和歌单创建者

b)、歌单歌曲列表

直接上图

这个页面也不是很难,大致分为两个部分:头部和歌单歌曲列表,头部包括歌单封面、歌单名称、创建者、描述、收藏量和评论数(点击创建者后进入用户详情页,点击评论进去歌单评论区)

6、歌单评论区

歌单评论区实现:

a)、显示歌单评论

完成图:

歌单评论区整体为一个评论列表组件,点击回复可以查看回复的评论

7、用户中心页

用户中心页实现:

a)、用户头像、昵称、关注数量、粉丝数和等级

b)、用户基本信息

c)、音乐品味

c)、创建的歌单和收藏的歌单

d)、点击歌单后根据id跳转到歌单详情页

完成图:

8、歌手榜单页

歌手榜单页实现:

a)、华语男歌手榜单(后期会进行优化,实现多种歌手榜单的切换)

完成图:


该页面比较简单,就是一个歌手列表,通过点击歌手列表项进入歌手详情页

9、歌手详情页

歌手详情页实现:

a)、歌手名称、歌手照片

b)、艺人百科

c)、歌手热门歌曲

b)、歌手专辑

完成图:


10、个人中心页

个人中心页实现:

a)、个人网易云头像和昵称

b)、创建的歌单和收藏的歌单

c)、点击歌单后根据id获取歌单详情

11、云音乐热歌榜

该页面主要包括两大部分,上方的搜索框和下方的音乐列表

音乐热歌榜实现

a)、点击搜索框进入音乐搜索页面

b)、音乐列表展示今日热歌,点击音乐列表项进入音乐详情页

12、搜索音乐

搜索音乐页面基本实现:

a)、搜索页面使用数据库存储历史记录,支持单条记录的删除以及清空所有历史记录,点击搜索历史再次根据关键词搜索文件。

b)、根据关键词搜索音乐并展示搜索结果条目数以及搜索结果。

c)、访问接口搜索耗时较长,设计“正在搜索”页面。

d)、搜索结果为零时,设计提醒搜索结果为零页面。

进入搜索音乐页

获取搜索框焦点后


当搜索后将搜索记录保存到历史记录列表,并搜索音乐

例:当搜索"IU"时

搜索结果:

13、音乐详情页

音乐详情页基本实现
a)、音乐播放
b)、歌词滚动显示
c)、用户评论

完成图:



整个页面分为两个部分:音乐播放器和用户评论

用户评论实现起来比较简单,在这就不说了

音乐播放器应该算整个app里面比较难的,所以我在这里一步一步分析

我在这将音乐播放器分为三个部分:上方的黑胶唱片旋转动画、中间的进度条和播放按钮、最下方的歌词字幕

1、黑胶唱片旋转动画

//旋转图片组件Widget _buildRotationTransition() {return Container(child: RotationTransition(//设置动画的旋转中心alignment: Alignment.center,//动画控制器turns: _animController,//将要执行动画的子viewchild: Container(width: 130,height: 130,child: ClipOval(child: Image.network(widget.music.picSmall),),),),);}

2、音乐进度条控制音乐播放

/// 播放进度条组件
typedef PlayOnTapCallback = void Function(AudioPlayerState _playerState);
typedef DurationOnTapCallback = void Function(Duration p);class MusicPlayerSlider extends StatefulWidget {String audioUrl;double volume;String onPlaying;Color color;bool isLocal;PlayOnTapCallback playOnTapCallback;// 点击播放暂停回调DurationOnTapCallback durationOnTapCallback;// 播放时间MusicPlayerSlider({@required this.audioUrl,this.volume : 1.0,this.onPlaying,this.color : Colors.white,this.isLocal : false,this.playOnTapCallback,this.durationOnTapCallback});@override_MusicPlayerSliderState createState() => _MusicPlayerSliderState();
}class _MusicPlayerSliderState extends State<MusicPlayerSlider> {AudioPlayer audioPlayer;Duration duration;Duration position;double sliderValue;// 音乐状态AudioPlayerState _playerState;@overridevoid initState(){super.initState();audioPlayer = new AudioPlayer();//durationHandler会回调音频总时长,positionHandler会回调播放进度audioPlayer.onPlayerCompletion.listen((event) {print("播放完成");});// 播放完成audioPlayer.onPlayerError.listen((e) {print(e);});// 播放错误audioPlayer.onAudioPositionChanged.listen((Duration  p) {// 播放时长改变setState(() {sliderValue = p.inSeconds/duration.inSeconds;});widget.durationOnTapCallback(p);});audioPlayer.onPlayerStateChanged.listen((AudioPlayerState s) {// 播放器状态改变setState(() {_playerState = s;});});audioPlayer.onDurationChanged.listen((Duration d) {// 设置歌曲时长setState(() => duration = d);});}//组件完全销毁时回调,主要在里面做一些移除监听的操作@overridevoid dispose(){audioPlayer.release();super.dispose();}// 播放音乐_play() {audioPlayer.play(widget.audioUrl,isLocal: widget.isLocal,volume: widget.volume);}// 暂停音乐_pause(){audioPlayer.pause();}// 停止音乐_stop(){audioPlayer.stop();}@overrideWidget build(BuildContext context) {return Container(child: Column(children: [Slider(onChanged: (newValue) {if (duration != null) {int seconds = (duration.inSeconds * newValue).round();audioPlayer.seek(Duration(seconds: seconds));}},value: sliderValue ?? 0.0,activeColor: widget.color,),Row(mainAxisAlignment: MainAxisAlignment.center,children: [IconButton(onPressed: (){if(_playerState == AudioPlayerState.PLAYING)_pause();else_play();print(_playerState);widget.playOnTapCallback(_playerState);},icon: _playerState == AudioPlayerState.PLAYING ? Icon(Icons.pause_circle_filled_outlined) : Icon(Icons.play_circle_fill),),IconButton(onPressed: (){_stop();setState((){sliderValue = 0.0;});print(_playerState);widget.playOnTapCallback(_playerState);},icon: Icon(Icons.stop),)],)],),);}}
// 播放器组件Widget _playWidget(){return Container(padding: EdgeInsets.only(bottom: 10.0),child: Stack(children: [Container(height: 200.0,decoration: BoxDecoration(image: DecorationImage(image: NetworkImage("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=4225846040,3889426807&fm=26&gp=0.jpg"),fit: BoxFit.cover,colorFilter: new ColorFilter.mode(Colors.black54,BlendMode.overlay,),),),), //黑胶唱片图片Container(height: 200.0,child: BackdropFilter(filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),child: Opacity(opacity: 0.2,child: Container(decoration: BoxDecoration(color: Colors.grey.shade900,),),),)), //模糊图层Column(children: [Container(padding: EdgeInsets.only(top: 30.0),child: _buildRotationTransition(),),Container(padding: EdgeInsets.only(top: 60.0),child: MusicPlayerSlider(audioUrl: widget.music.playUrl != null ? widget.music.playUrl : "",playOnTapCallback: (state){if(state == AudioPlayerState.PLAYING){_animController.stop();// 停止}else if(state == AudioPlayerState.PAUSED){_animController.forward();//正向开始}else if(state == AudioPlayerState.STOPPED){_animController.reset();// 重置_animController.forward();}else{// null_animController.forward();//正向开始}},durationOnTapCallback: (p){setState(() {_inSeconds = p.inSeconds;});},),)],),//播放进度条 和 旋转图片],),);}

3、歌词字幕滚动显示

///字幕控件
class Subtitle extends StatefulWidget {List<LyricEntry> data;TextStyle selectedTextStyle;TextStyle unSelectedTextStyle;double diameterRatio;double itemExtent;int inSeconds;Subtitle(this.data,this.inSeconds,{this.selectedTextStyle,this.unSelectedTextStyle,this.diameterRatio,this.itemExtent});@override_SubtitleState createState() => _SubtitleState();
}class _SubtitleState extends State<Subtitle> {int _currentIndex;ScrollController _controller;// 控制滑动的计时器。Timer _timer;@overridevoid initState(){super.initState();_initController();// 初始化滚动监听_startTimer();}// 开启计算器void _startTimer() {// 计时器(`Timer`)组件的定期(`periodic`)构造函数,创建一个新的重复计时器。_timer = Timer.periodic(Duration(microseconds: 300), (timer) {if(widget.inSeconds != null){for(int i = 0 ;i < widget.data.length;i++){if(widget.inSeconds == widget.data[i].time){_controller.animateTo((45 * i).toDouble(), duration: Duration(milliseconds: 300), curve: Curves.linear);}}}});}void _initController(){_controller = new ScrollController();_controller.addListener(() {// 监听滚动位置 来将歌词高亮setState(() {_currentIndex = _controller.offset ~/ 45;});});}@overridevoid dispose() {//为了避免内存泄露,需要调用_controller.dispose_controller.dispose();super.dispose();}@overrideWidget build(BuildContext context) {if (widget.data == null || widget.data.length == 0) {return Container(padding:EdgeInsets.only(top:50.0),alignment: Alignment.center,child: Text("加载歌词失败"),);}return ListWheelScrollView.useDelegate(controller: _controller,diameterRatio: widget.diameterRatio,itemExtent: widget.itemExtent,childDelegate: ListWheelChildBuilderDelegate(builder: (context, index) {return Container(alignment: Alignment.center,child: Text('${widget.data[index].word}',style: _currentIndex == index? widget.selectedTextStyle: widget.unSelectedTextStyle,),);},childCount: widget.data.length),);}}

歌词根据音乐时间滚动效果

歌词格式:时间戳+歌词
00:00 歌词:
00:25 我要穿越这片沙漠
00:28 找寻真的自我
00:30 身边只有一匹骆驼陪我
00:34 这片风儿吹过
00:36 那片云儿飘过

首先将歌词转换成lyricEntry类 类包含秒数和歌词,即将时间戳换成秒数
例:

我是通过进度条的回调函数将音乐播放的时间赋值给父组件的inSeconds,然后将这个参数传给字幕组件。

然后设置定时器来判断歌词应该滚动到哪个地方

在滚动监听里判断满足条件时歌词高亮

14、App图标和名称

android :

Flutter开发一个云音乐APP(包含接口地址,亲测可用)相关推荐

  1. 阿里云 部署SpringBoot和Vue项目 亲测可用(第一次部署经验贴)

    阿里云 部署SpringBoot和Vue项目 亲测可用!第一次部署经验贴! 前言:与伙伴一起写了一个项目,但是由于老师要我们部署到服务器上,而我从未有部署过,查看了csdn很多博客,试了好多篇,才成功 ...

  2. qq音乐常用接口整理——亲测可用

    1. 搜索音乐--可以获取歌曲id,歌手,专辑名,歌曲图片id等等等... var urlString = 'http://s.music.qq.com/fcgi-bin/music_search_n ...

  3. 豆瓣电影最新API接口(亲测可用)

    前言: 开源的豆瓣接口(经测试可使用),最底下是整理的可使用的方法 最新资料1:https://github.com/iiiiiii1/douban-imdb-api​​​​​​​ 官方文档入口:官方 ...

  4. 解决 s3.amazonaws.com 亚马逊云文件下载慢的方法(Ubuntu)——亲测可用2021

    1.ping s3.amazonaws.com 2. 将 52.217.106.198 s3-1.amazonaws.com 加入hosts` 52.217.106.198 s3-1.amazonaw ...

  5. java计算机毕业设计vue开发一个简单音乐播放器(附源码、数据库)

    java计算机毕业设计vue开发一个简单音乐播放器(附源码.数据库) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + Mysql + HBuilderX(Webstorm也行)+ Ec ...

  6. FastAPI:快速开发一个文本转语音的接口

    这段音频就是本文的接口生成的. Python Web 开发方面有一个很重要的环节就是开发接口,开发接口性能最好的工具就是闪电侠 FastAPI[1],正如它的名字一样,是非常快的 API.当然,还有一 ...

  7. 基于Flutter开发网站转换成APP源代码 网站生成APP源代码带控制端

    这是一款输入域名直接把网站转换成APP的平台源码,App的开发语言使用Flutter,控制端(平台端)的开发语言是PHP,且附带有App开发工具,开发工具使用的是AndroidStudio. 部署本套 ...

  8. Android音乐播放器手机乐园,思约云音乐APP

    思约云音乐APP是一款专业的听音乐的APP,可以为我们提供充足的音乐曲库和下载功能,在APP里你可以找到各种好用的音乐内容,在线听歌或者是离线缓存都可以,上百万手免费音乐随便听,总有一款是你喜欢的,非 ...

  9. 一个云本地文件包含漏洞,影响世界一流公司

    本文讲的是一个云本地文件包含漏洞,影响世界一流公司,先通过一张截图看一下影响范围吧 本地文件包含是在Oracle Responsys的云服务中存在的.什么是Responsys?它是企业级基于云的B2C ...

  10. java计算机毕业设计vue开发一个简单音乐播放器源码+mysql数据库+系统+lw文档+部署

    java计算机毕业设计vue开发一个简单音乐播放器源码+mysql数据库+系统+lw文档+部署 java计算机毕业设计vue开发一个简单音乐播放器源码+mysql数据库+系统+lw文档+部署 本源码技 ...

最新文章

  1. 如何根据keras的fit后返回的history绘制loss acc曲线
  2. android运行内存与存储内存,运行内存和机身内存的区别 这些知识你知道吗
  3. [AHOI2014/JSOI2014]支线剧情
  4. 如何减小电压跟随器输出电阻_机器人如何保护电池的电源管理系统 免受热坏?(附:PDF文档下载)...
  5. 阿里云CDN技术掌舵人文景:相爱相杀一路狂奔的这十年
  6. Java和pathion_Spring配置中的classpath:与classpath*:的区别
  7. 中国巨头竞相复制Clubhouse:一场无关输赢的竞赛
  8. OpenShift Security (7) - 风险合规评估
  9. 最新美团JS逆向分析(_token参数)
  10. 【ms access】SQL 引用外部表
  11. 趣图:SQL 版的喝椰汁,没想到吧
  12. keyup常用事件_KeyUp 事件
  13. redis用zookeeper实现自动主从同步,切换
  14. godot常用的一些概念、组件(整理于官方教程)
  15. 走出NASA,向大地“下战书”,他要用卫星遥感数据改变中国农业
  16. Docker 学习前置,网络IP地址以及交互
  17. U盘中毒,文件夹或文件打不开的解决方法--实用
  18. 什么是MyBatis?怎么操作MyBatis?
  19. 机房服务器系统监控软件,机房服务器系统监控软件
  20. 我的职业生涯中所获取的职业技能证书

热门文章

  1. 木心先生的句子,不仅美,而且富有深意! ​​​
  2. 1-10000的素数 java_java实验题(1-10000之间的素数和)
  3. Python每日笔记打卡_day2
  4. reactjs前端实现文件新窗口下载
  5. 佛罗里达大学计算机专业世界排名,2020年中佛罗里达大学排名TFE Times美国最佳计算机科学硕士专业排名第107...
  6. 如何在word中的方框里打钩
  7. Windows 打开和关闭默认共享方法汇总
  8. 《程序猿的搬砖生活》八、学生时代最后的“疯狂”
  9. 性能测试包括哪些方面?分类及测试方法有哪些?
  10. 一位优秀的学弟,计算机2019保研经历分享(北大信科、清华计算机系)