前言

这是这个系列的第二篇文章,和第一篇文章相同的是Demo中的资源文件和一些关键代码是搜索和学习得来的。一是因为没有相关的资源文件,譬如音乐文件、歌词文件、歌曲封面等;二是着实有点力有未逮的感觉(ps:在Demo具体功能中体现出来的就是歌词随着歌曲播放进度的不断滚动以及颜色的渐变,主要的原因是不知道lrc的文件内容以及一些API的用法,后面将会谈到。)。为了“发现更大的世界”(ps:其实就想捡点便宜),就百度了一番,在这里找到了“好东西”!笔者学习了其中的关键代码,并结合自己的想法改造了代码和实现了一些新的功能,像歌词颜色渐变之类的。如果你愿意继续看下去的话,那咱们接着慢慢讲!

三军未动,粮草先行

Demo的功能界面展示在原作者博客中可以看到,这里就不赘述了。既然想要做一个音乐播放器,如果只有音乐的声音,那就太单调了,再次感谢原作者的分享精神!有了上面的一系列“粮草”(包括一些UI的设计),就不至于在“半路”上被“饿死”了!

呃!等等,先来看看以lrc作为后缀的文件里面是怎样的“乾坤”:

简单来说,我们可以从中得到时间信息和歌词信息,只不过它们不是唾手可得,而需要通过一定的数据拆分和加工。每两行的时间差可以帮助我们控制歌词颜色渐变的时间,但有一个细节应当注意:需要保证下一行的歌词信息存在。得益于歌词文件的最后一行为空白行,时间的确定只需让当前一行减去下一行的开始时间即可。

显然,这样做能实现的只是时间均匀的过渡,要实现向酷狗音乐那样有快有慢的节奏从技术上来看只需要调整时间分段即可,但是从lrc的文件中我们得不到关于每段歌词的时间分量。如果你用酷狗下载过音乐歌词的话,你会发现它是krc为后缀的文件。可以想到的一点是这里面一定有关于每一行歌词各部分时间分量的控制信息。在早期的歌词显示中,歌词都是采用这样均匀的渐变。估计在智能手机普及之后,又有公司(比如酷狗音乐)花了精力在歌词中融入了更多的信息来让用户获得更好的使用体验,也算是提高市场竞争力的一种手段吧,像虾米音乐的歌词还是沿用均匀渐变这一设计。貌似krc只能用酷狗音乐软件,还要求是在播放状态读取,这是对自我知识产权的保护。没办法了,我们就将就lrc用用吧!

遵循Demo数据从简原则,笔者就用plist文件来展示歌曲的详细信息,下面是一首歌曲的信息:

同时将它与Music类关联起来:

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *icon;
@property (nonatomic, copy) NSString *filename;
@property (nonatomic, copy) NSString *lrcname;
@property (nonatomic, copy) NSString *singer;
@property (nonatomic, copy) NSString *singerIcon;

这样就设计好了音乐展示页面的数据模型了,并构造了一个MusicTool类,来避免控制器之间直接横向依赖,也就是笔者在这篇文章里面所转载的文章中所说的“依赖下沉”。MusicTool用来获取所有音乐,并通过文件名获取当前要播放的音乐。下一曲和上一曲就简单的通过增加和减少当前音乐索引值,在所有的音乐中用该值索引即可。

Demo中将音乐的播放功能通过一个AudioManager类的单例来管理,包含播放、暂停、停止三个业务。

知道了lrc歌词文件的内容,我们就能合理地设计数据模型来从文件中解析出实现业务功能所需要的数据了。Demo中以LyricLines类的实例来集合一句歌词中需要被外界的访问的各类信息,就像这样:

@interface LyricLines : NSObject@property (copy, nonatomic) NSString *word;@property (assign, nonatomic) CGFloat gradientTime;
@property (assign, nonatomic) CGFloat time;
@property (assign, nonatomic) CGRect stringRect;- (NSMutableArray<LyricLines *> *)lyricLinesWithFileName:(NSString *)fileName;
  • word是具体的歌词

  • gradientTime是当前歌词渐变时长

  • time是当前歌词的时间信息

  • stringRect是歌词的长度尺寸(ps:这个用于设置渐变时前景UILabel的frame,这是根据歌词文字加上cell的宽高计算出的,目的是更加精确地控制渐变过程,即让前景UILabel大小与歌词文字完全重合)

下面的这个静态方法可以根据歌词文件名返回对应歌曲的歌词信息数组,它主要包含了这样一些逻辑:

  1. 通过NSBundle的动态方法URLForResource:得到歌词文件的NSURL,再用NSString的动态方法stringWithContentsOfURL:encoding:error:获取歌词的字符串流。

  2. 以“\n”分割该字符串流,分割后将生成一个数组,数组的每个下标元素均代表一行歌词。此时,时间信息与歌词信息是一个整体,即字符串。

  3. 使用枚举器遍历数组,即enumerateObjectsUsingBlock:,分割当前行歌词时间分量、渐变时长、歌词信息和歌词长度,然后存入一个LyricLines实例对应的属性中,并加入到一个临时可变数组,最后返回该数组即可。

在计算时间时,用到了一个自定义的时间转换工具TimeConvertTool——用于将时间字符串与时间浮点数相互转换,下面是它对外的接口:

@interface TimeConvertTool : NSObject+ (NSString *)timeNormalStringWithTime:(NSTimeInterval)time;+ (NSTimeInterval)timeWithTimeDetailString:(NSString *)timeDetailString;@end
Just do a sample demo

这里面设计了两个控制器MusicViewControllerPlayingViewController。前者是歌曲信息的展示控制器,后者是播放界面控制器。(ps:起初起名字没怎么思考,后面当发现命名有所不当时,想改时又发现成本太大,就放置了这一想法。命名也是学问啊!)

展示控制器的界面是这个样子的:

黄色的圆圈表示这首歌正在播放,至于画圈的功能交给了ImageTool类来完成,它里面只有一个静态方法:

+ (UIImage *)circleImageWithName:(NSString *)imageName BorderWidth:(CGFloat)borderWidth BorderColor:(UIColor *)borderColor;

为了在播放控制器中选择了上一曲或者下一曲能够在歌曲展示的控制器中有所体现,笔者这里设计了一个简单的代理,并把它放在Music类中(ps:实际上这种做法不怎么好,因为它从本质上来说只是一个接口协议而已。当一个对象需要遵循这个协议时,只需要导入协议文件就可以了,其他的对它来说都多余了,所以最好将它放入一个单独的头文件。):

@protocol MusicSwithDelegate <NSObject>@required- (void)switchMusicWithCurrentIndex:(NSInteger)currentIndex DestinedIndex:(NSInteger)destinedIndex;@end

在给MusicCell设置数据模型music时,同时将它的delegate设置为展示控制器就可以了。

进入播放界面后,看到的界面是下面的这个样子:

上面的两个按钮分别作为退出播放视图和显示歌词的功能键。进度条上的白色按钮显示当前的歌曲已经播放的时间,按住它可以拖动并改变时间进度,点击灰色的进度条也能实现此功能。

播放控制器关联了一个歌词视图控制器,而不是一个歌词视图,因为歌词界面也设计了比较多的逻辑。(ps:这里能知道一点这样做的原因)如果播放控制器视图中没有包含了歌词视图,则在点击歌词按钮时将LyricsViewController作为子控制器加到播放控制器中,然后将它的视图也加到这上面来;如果包含了,那么就做相反的工作。

在歌词控制器中歌词视图的单元格类LyricCell想向外暴露的接口shouldGradient用于控制单元格是否执行渐变效果,当indexPath.row等于_currentIndex时即开始渐变。

_currentIndex是这样计算的:

- (void)setCurrentTime:(NSTimeInterval)currentTime {//when play next or previous song, set _currentIndex is equal to 0if (_currentTime > currentTime) {_currentIndex = 0;}_currentTime = currentTime;for (NSInteger i = _currentIndex; i < self.lyricLines.count; i ++) {LyricLines *currentLine = self.lyricLines[i];CGFloat currentLineTime = currentLine.time;CGFloat nextLineTime = 0;if (i + 1 < self.lyricLines.count) {LyricLines *nextLine = self.lyricLines[i + 1];nextLineTime = nextLine.time;if (currentTime >= currentLineTime && currentTime <= nextLineTime && _currentIndex != i) {NSArray *reloadLines = @[[NSIndexPath indexPathForRow:_currentIndex inSection:0], [NSIndexPath indexPathForRow:i inSection:0]];_currentIndex = i;[self.lyricsView reloadRowsAtIndexPaths:reloadLines withRowAnimation:UITableViewRowAnimationFade];[self.lyricsView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:_currentIndex inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];break;}}}
}

CALayer有一个属性mask,它的类型是CALayer,就像它的名字一样,它是一个图层的掩模层,它的frame、anchorPoint、position之间满足这样的关系(更多信息可参考这里):

frame.origin.x = position.x - anchorPoint.x * bounds.size.width
frame.origin.y = position.y - anchorPoint.y * bounds.size.height

有一点让人奇怪的是它的frame反而是被其mask的图层的可见区域,而不是被遮掩的区域。

这样就不难理解为什么要像下面这样设置了(position默认值为宽高的一半,anchorPoint默认值为(0.5, 0.5)):

_gradientMaskLayer.anchorPoint = CGPointMake(0, 0.5);self.gradientLayer.position = CGPointMake(0, self.frontLabel.bounds.size.height / 2);self.gradientLayer.bounds = CGRectMake(0, 0, 0, self.frontLabel.bounds.size.height);

我们就可用动画的形式来控制这个遮罩层的宽度来变相实现歌词的颜色渐变了,而想要不同的颜色改变前景文字颜色就可以了。(ps: 如果设置的是渐变层的frame,就不需要设置position,position本质上是anchorPoint的实际坐标)

玩具车用了之后,遥控器上还能接着用

其实我们就只是想实现一个远程控制而已,没有玩具车。:)

PlayingViewController中有这样一段代码:

+ (void)initialize {AVAudioSession *session = [AVAudioSession sharedInstance];[session setCategory:AVAudioSessionCategoryPlayback error:nil];[session setActive:YES error:nil];
}

你没看错!这只是用来设置音乐后台播放的会话策略而已,遥控器还没“买”。

UIResponder有这样一个方法:

- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

这个方法用于远程事件的处理,关于音乐播放的远程事件主要有以下几种子类型:

UIEventSubtypeRemoteControlPlay              = 100,
UIEventSubtypeRemoteControlPause             = 101,
UIEventSubtypeRemoteControlNextTrack         = 104,
UIEventSubtypeRemoteControlPreviousTrack     = 105
......

当接收到远程事件时,根据事件的子类型选择执行相关方法即可。

控制器遵循并实现了AVAudioPlayerDelegate协议,其中一点应用就是当前歌曲播放完成之后自动播放下一首,另外就是中断处理程序,这里就只完成了简单暂停播放:

#pragma mark - AVFundation Delegate- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {[self nextSong];
}- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player {[self playOrPauseSong];
}- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player {[self playOrPauseSong];
}

你可能注意到,当使用音乐软件时,即使锁屏,也会有相关的信息在锁屏界面展示。这需要MPNowPlayingInfoCenter的支持,如果需要监听远程控制事件,还需要这样做:

[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];

Demo中在开始播放音乐方法中定义了一个私有方法:updateLockedScreenMusic,这个方法里完成了上面的任务。

总结

这篇文章到此笔者能说的大致都写出来了,中间断断续续了一段时间。因为期末临近,加上各种实验课,真是忙的够呛!完整的Demo在这里可以找到,有需要的朋友可以看看。

参考资料:

紫忆:ios开发——一个音乐播放器的设计与实现

一个音乐播放器的踩坑实践相关推荐

  1. 基于React开发一个音乐播放器

    同时支持 Mac 与 Windows 系统. 下载地址 掘金链接 项目使用 electron 作为外壳,webpack 作为打包工具,核心技术包括 React + Redux + React-rout ...

  2. 听音乐不过瘾?自制一个音乐播放器!| 原力计划

    作者 | 灰小猿 责编 | 夕颜 出品 | CSDN博客 最近在学习C#的GUI编程时想着自制一个播放器,说干就干. 其实C#除了在游戏开发上具有显著优势以外,在winform交互页面设计和web网站 ...

  3. 用vue简单写一个音乐播放器

    简单地写一个功能比较全的音乐播放器 前言 因为音乐播放器是一个很可能在项目遇到的东西,早写总比晚写好.趁没事先写个. 思路 一个音乐播放器该有的东西: 封面,歌名,专辑,作者 控制器(上一首,下一首, ...

  4. 如何使用uni-app做一个音乐播放器

    如何使用uni-app做一个音乐播放器 uni-app提供给我们非常棒的API,可以做出很好看的自定义样式的音乐播放器 好的编译器可以让我们的项目事半功倍,HBuilderX可以很方便的创建uni-a ...

  5. 写一个音乐播放器的微信小程序

    要创建一个音乐播放器的微信小程序,您需要熟悉微信小程序的开发环境和语言(如 JavaScript 和 WXML/WXSS). 具体来说,您需要做以下几件事: 设计音乐播放器的用户界面,并使用 WXML ...

  6. html5开发一个音乐播放器,HTML5开发学习(1):使用aduio标签打造音乐播放器

    关于html5的炒作已经有一段时间了,小弟亦是个跟风之人,对该新鲜事物也充满好奇和期待.本文为该系列(html5尝鲜)第一节,先以一个简单的demo开胃,希望能勾起各位同学对html5的兴趣和关注. ...

  7. 教你用树莓派Python打造一个音乐播放器

    买了个树莓派3B+,装好系统后灰落了好厚一层都不知道要干嘛...最近突发奇想:用树莓派做一个音乐播放器,每天6:30-7:20自动播放英语听力,强迫自己练习英语,也治治自己的懒床习惯,平时也可以用来听 ...

  8. 使用小程序制作一个音乐播放器

    此文主要通过小程序制作一个音乐播放器,实现轮播.搜索.播放.快进.暂停.上一曲.下一曲等功能. 一.创建小程序 二.设计页面 三.接口渲染 一.创建小程序 访问微信公众平台,点击账号注册. 选择小程序 ...

  9. 网页实现一个音乐播放器

    怎样在网页实现一个音乐播放器(js html css)(网页实现音乐播放) 标题 求大佬帮着写一个页面实现音乐播放

最新文章

  1. 每个程序员都应该读的书
  2. rpm包安装mysql5.6
  3. python 线程同步_Python并发编程-线程同步(线程安全)
  4. python输入成绩求总分和平均分_python脚本如何输入成绩求平均分?
  5. vmstat工具详解
  6. MySQL如何访问Postgres
  7. 今天碰到的angular 中的一个小坑
  8. 设计模式学习02-观察者模式
  9. PHP开源CRM客户管理系统源码介绍分享
  10. 代理的原理及类型总结
  11. 邮件服务器需要445端口,445端口是什么服务端口(2)
  12. 微软Surface Go 体验:可以当平板使用的便携笔记本电脑
  13. html自动关闭当前页面,html如何关闭当前页面
  14. 人行征信2.0对接服务:全业务种类数据,精细您的征信业务管理!
  15. GEE入门【4】| 矢量数据FeatureCollection(行政区划分)
  16. NSIS制作自己的安装包
  17. python画图基础
  18. oracle日志在哪里看,Oracle日志文件管理与查看
  19. 金山pdf批注更改批注作者_Linux上的PDF批注工具
  20. 2019-一个基于CNN的多模式工具来保证视频的正确性A MULTIMODAL CNN- BASED TOOL TO CENSURE INAPPROPRIATE VIDEO SCENES

热门文章

  1. C语言练习题1:英文字母大小写转换
  2. 一句话解释什么是 风投,融资,泡沫,跟投,平台,对冲,上市,P2P,维权
  3. 梁实秋《时间即生命》
  4. win11: 该文件没有与之关联的应用来执行该操作及正确删除桌面快捷方式小图标
  5. PS制做心形app图标
  6. 水墨屏超高频电子标签|RFID电子纸之可视化标签组态软件操作流程
  7. 自然语言处理从入门到应用——词向量的评价方法
  8. 代码重构(一):函数重构规则
  9. qt制作简易视频截帧软件
  10. NFT年度回顾:市场格局、品牌采用与版税争议