5.5 实战:“天后王菲”音频播放器的开发 在本小节中读者将通过开发一款音乐播放器类应用程序学习到更多技巧,通过歌词同步引擎的开发,读者将初步掌握iOS中数据解析的相关方法。 5.5.1 工程搭建与LRC歌词文件简介 一款流行的音乐播放器软件,在播放音乐时同步显示歌词是必备的功能,其实这些播放器软件都实现了一个歌词解析引擎,通过歌词解析引擎将LRC文件解析为程序中需要的歌词对象。 LRC是Lyric单词的缩写,是音频同步歌词文件的一种协议格式。LRC文件中通过标签的形式将歌曲的专辑、歌手、每个时间点对应的歌词记录其中,音频播放器通过解析这些数据来同步显示歌词。一个LRC歌词文件其内容格式大致如下所示: [ti:匆匆那年] [ar:王菲] [al:电影《匆匆那年》主题曲] [t_time:(04:08)] [00:32.16] 匆匆那年 我们究竟说了几遍 [00:34.82] 再见之后再拖延 [00:37.62] 可惜谁有没有爱过 [00:39.37] 不是一场七情上面的雄辩 [00:43.42] 匆匆那年 我们一时匆忙撂下 [00:45.82] 难以承受的诺言 只有等别人兑现 [00:54.69] 不怪那吻痕 还没积累成茧 [01:00.44] 拥抱着冬眠 也没能羽化再成仙 [01:05.74] 不怪这一段情没空反复再排练 [01:11.24] 是岁月宽容恩赐 反悔的时间 [01:22.38] 如果再见不能红着眼 [01:25.38] 是否还能红着脸 [01:28.24] 就像那年匆促刻下 [01:30.14] 永远一起那样美丽的谣言 [01:33.49] 如果过去还值得眷恋 [01:36.89] 别太快冰释前嫌 [01:39.44] 谁甘心就这样 [01:42.39] 彼此无挂也无牵 [01:45.93] 我们要互相亏欠 [01:50.94] 要不然凭何怀念 [02:02.25] 匆匆那年 我们见过太少世面 [02:04.79] 只爱看同一张脸 [02:07.65] 那么莫名其妙 那么讨人欢喜 [02:10.25] 闹起来又太讨厌 [02:13.30] 相爱那年活该匆匆 [02:15.51] 因为我们不懂顽固的诺言 [02:18.85] 只是分手的前言 [02:24.65] 不怪那天太冷 泪滴水成冰 [02:30.55] 春风也一样没吹进凝固的照片 [02:35.70] 不怪每一个人没能完整爱一遍 [02:41.35] 是岁月善意落下 残缺的悬念 [02:52.36] 如果再见不能红着眼 是否还能红着脸 歌词文件中ti标签对应歌曲的名称,ar标签对应歌手名称,al标签对应歌曲专辑。t_time对应歌曲的时间,之后的时间标签代表某一时刻对应的歌词。 使用Xcode创建一个名为MyPlayer的工程,向其中导入一些音频文件及其对应的LRC歌词文件。需要注意的是,LRC文件的文件名要与对应的歌曲名相同,便于在解析时将歌词文件与歌曲对应。 在向工程中添加大量文件时,可以通过建立新的引用目录使工程的目录结构看起来整齐一些,在Xcode文件导航区单击右键,选择New Group选项即可新建一个引用目录,如图5-30所示。 5.5.2 LRC歌词解析引擎的设计 在Xcode中创建两个引用目录Song和LRC,分别用来存放歌曲文件与歌词文件。设计一个新的类作为每行歌词的数据模型,使用Xcode新建一个类文件,命名为LRCItem,使其继承于NSObject。 在LRCItem.h文件中添加如下方法和属性的声明。 @interface LRCItem : NSObject @property (nonatomic) float time; @property (nonatomic,copy) NSString *lrc; //排序方法 - (BOOL)isTimeOlderThanAnother:(LRCItem *)item; @end 上面声明的属性和方法中,float是此行歌词的出现时间,lrc是此行歌词具体的文本数据。IsTimeOlderThanAnother:方法用于行歌词数据模型的排序,这个方法将按照时间先后进行排序。 在LRCItem.m文件中添加方法的实现代码。 - (BOOL)isTimeOlderThanAnother:(LRCItem *)item{ return self.time > item.time; } 再新建一个类文件命名为LRCEngine作为歌词解析引擎,使其继承于NSObject。在LRCEngine.h文件中引入LRCItem类的头文件: #import "LRCItem.h" 在LRCEngine.h文件中声明如下的属性与方法: @interface LRCEngine : NSObject -(instancetype)initWithFile:(NSString *)fileName; @property(nonatomic,strong)NSString * author; @property(nonatomic,strong)NSString * albume; @property(nonatomic,strong)NSString * title; -(void)getCurrentLrcInLRCArray:(void(^)(NSArray * lrcArray,int currentIndex))handle atTime:(float)time; @end 在上面代码中,initWithFile:方法用于进行歌词引擎的初始化,flieName参数为歌词文件的名称。author属性为歌手姓名。albume属性为专辑的名称。title属性为歌曲的名称。getCurrentLRCInLRCArray:atTime:方法为歌词引擎的核心方法,其通过传入一个时间点的值来获取当前对应的歌词,在handle函数块中将传入两个参数,一个是已经按时间排序的每行歌词数据的数组,一个是当前对应的歌词在数组中的位置。 在LRCEngine.m文件中声明一个可变数组用于存放每行歌词数据,代码如下: @implementation LRCEngine { NSMutableArray * _lrcArray; } @end 实现LRCEngine类的初始化方法,代码如下: -(instancetype)initWithFile:(NSString *)fileName{ if (self=[super init]) { _lrcArray = [[NSMutableArray alloc]init]; [self creatDataWithFile:fileName]; } return self; } 上面方法中进行数组对象的初始化操作,creatDataWithFile:方法的实现如下: -(void)creatDataWithFile:(NSString *)fileName{ //读取文件 NSString * lrcPath = [[NSBundle mainBundle]pathForResource:fileName ofType:@"lrc"]; NSError * error; NSString * dataStr = [NSString stringWithContentsOfFile:lrcPath encoding:NSUTF8StringEncoding error:&error]; //去掉/r NSMutableString * tmpStr = [[NSMutableString alloc]init]; NSArray * tmpArray = [dataStr componentsSeparatedByString:@"\r"]; for (int i=0; i='0'&&c<='9') { //是歌词数据 [self getLrcData:lrcStr]; }else{ //是文件信息数据 [self getInfoData:lrcStr]; } } //进行歌词数据的重新排序 [_lrcArray sortedArrayUsingSelector:@selector(isTimeOlderThanAnother:)]; } getLrcData:方法的实现如下: -(void)getLrcData:(NSString *)lrcStr{ //按照]进行分割 NSArray * arr = [lrcStr componentsSeparatedByString:@"]"]; //解析时间 同一行歌词可能对应多个时间 最后一个元素是歌词 for (int i=0; itime) { index=i-1; break; } } if (index==-1) { //第一条数据 index=0; }else if (index==-2){ //没有更大的时间了 最后一条数据 index=(int)_lrcArray.count-1; } handle(_lrcArray,index); } 为了验证LRC歌词引擎是否正常工作,在ViewController.m文件中引入LRCEngine类的头文件并在viewDidLoad方法中编写如下代码: - (void)viewDidLoad { [super viewDidLoad]; LRCEngine * engine = [[LRCEngine alloc]initWithFile:@"匆匆那年"]; [engine getCurrentLRCInLRCArray:^(NSArray *lrcArray, int currentIndex) { if (lrcArray) { NSLog(@"%@\n=======\n%@",lrcArray,[lrcArray[currentIndex] lrc]); } } atTime:100]; } 上面的代码中获取到歌词文件《匆匆那年》第100秒时的歌词,打印结果如图5-31所示,则说明LRC歌词引擎工作顺利正常。 图5-31 LRCEngine歌词引擎的工作打印调试 5.5.3 核心播放器引擎的设计 本节将再封装一个模块作为应用的核心播放器引擎,这个引擎应该可以满足常规的音频播放需求,例如循环播放、随机播放、上一曲和下一曲等。在设计之前,先将工程设置为支持后台音频播放,其实在Xcode中设置支持后台播放的方法除了前面介绍的配置info.plist文件外,还可以通过另一种方式实现。 单击工程文件,选择其中的Capabilities项,在其中找到Background Modes一项并将其打开,在后台运行模式中勾选支持音频后台播放的选项,过程如图5-32所示。 在工程中创建一个新的类文件,使其继承于NSObject类,将其命名为MyMusicPlayer作为核心音频播放引擎类。在MyMusicPlayer.h文件中引入如下头文件: #import 还需要在MyMusicPlayer.h文件中声明一个协议,这个协议中约定当一个音频文件播放完之后的代理回调方法,提供给外界进行逻辑操作。代码如下: @protocol MyMusicPlayerDelegate -(void)musicPlayEndAndWillContinuePlaying:(BOOL)play; @end 协议中musicPlayEndAndWillContinewPlaying:方法当一个音频播放完毕之后执行,其中传入的BOOL值参数决定是否自动播放下一个音频数据。 图5-32 设置应用程序支持后台运行模式 在MyMusicPlayer.h文件中声明如下属性与方法: @interface MyMusicPlayer : NSObject //歌曲名数组 @property(nonatomic,strong)NSArray * songsArray; //对应歌曲的歌词名数组 @property(nonatomic,strong)NSArray * lrcsArray; //是否循环播放 @property(nonatomic,assign)BOOL isRunLoop; //是否随机播放 @property(nonatomic,assign)BOOL isRandom; //音频播放器是否正在播放音频 @property(nonatomic,assign)BOOL isPlaying; //代理对象 @property(nonatomic,weak)id delegate; //获取当前播放的是第几个音频 @property(nonatomic,assign)int currentIndex; //当前播放的音频文件的时长 @property(nonatomic,assign)int currentSongTime; //当前播放的音频文件已经播放的时长 @property(nonatomic,assign)int hadPlayTime; //开始播放 -(void)play; //暂停播放 -(void)stop; //进行继续播放与暂停播放的切换 -(void)playOrStop; //上一曲 -(void)lastMusic; //下一曲 -(void)nextMusic; //停止播放 -(void)end; //播放指定的音频文件 -(void)playAtIndex:(int)index isPlay:(BOOL)play; @end 在前面的章节有介绍,关于音频播放的后台交互与耳机线控的操作是在AppDelegate类中进行的,因此开发者需要将MyMusicPlayer对象与程序的AppDelegate对象进行关联,便于后台交互操作的下发到具体视图控制器中,在MyMusicPlayer.m文件中引入如下头文件: #import "AppDelegate.h" 在AppDelegate.h文件中导入MyMusicPlayer.h头文件并添加如下属性: @property (nonatomic,strong)MyMusicPlayer *play; 在MyMusicPlayer.m文件中声明如下的内部属性: @implementation MyMusicPlayer { AVAudioPlayer * _player; NSTimer * _timer; } @end 在上面声明的属性中,_player用于处理音频的播放,_timer用于进行播放时间的更新。 在MyMusicPlayer.m文件中实现类的初始化方法,如下所示: - (instancetype)init { self = [super init]; if (self) { _timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0 target:self selector:@selector(update) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; AppDelegate * delegate =[UIApplication sharedApplication].delegate; delegate.play=self; } return self; } 在上面的初始化方法中对定时器进行创建并将当前对象与程序的AppDelegate对象进行了关联。 实现定时器的刷新方法update如下所示: -(void)update{ if (_player) { _hadPlayTime = _player.currentTime; } } 实现在MyMusicPlayer.h文件中声明的相关方法如下所示: //进行播放与暂停的切换 -(void)playOrStop{ //先判断是否正在播放 if (self.isPlaying) { //已经在播放则进行停止播放操作 [self stop]; }else{ //没有在播放则进行播放操作 [self play]; } } -(void)play{ //判断AVAudioPlayer对象是否存在 if (_player!=nil) { [_player play]; _isPlaying=YES; return; }else{ //从歌曲数组中读取第一个元素 NSString * path = [[NSBundle mainBundle]pathForResource:[self.songsArray objectAtIndex:0] ofType:@"mp3"]; NSURL * url = [NSURL fileURLWithPath:path]; _player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil]; _player.delegate=self; [_player play]; _isPlaying=YES; _currentIndex=0; _currentSongTime=_player.duration; } } -(void)stop{ if (_player.isPlaying) { [_player stop]; _isPlaying=NO; } } -(void)end{ [_player stop]; _isPlaying=NO; _player=nil; } -(void)playAtIndex:(int)index isPlay:(BOOL)play{ [_player stop]; _isPlaying=NO; _player = nil; NSString * path = [[NSBundle mainBundle]pathForResource:[self.songsArray objectAtIndex:index] ofType:@"mp3"]; NSURL * url = [NSURL fileURLWithPath:path]; _player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil]; _player.delegate=self; if (play) { [_player play]; _isPlaying=YES; } _currentIndex=index; _currentSongTime = _player.duration; } -(void)nextMusic{ BOOL play = _player.isPlaying; [_player stop]; _isPlaying=NO; _player=nil; //是否是最后一曲 if (_currentIndex0) { _currentIndex--; }else{ _currentIndex=(int)_songsArray.count-1; } if (self.isRandom) { unsigned long max = self.songsArray.count; _currentIndex = arc4random()%max; } NSString * path = [[NSBundle mainBundle]pathForResource:[self.songsArray objectAtIndex:_currentIndex] ofType:@"mp3"]; NSURL * url = [NSURL fileURLWithPath:path]; _player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil]; _currentSongTime=_player.duration; _player.delegate=self; if (play) { [_player play]; _isPlaying=YES; } } 在MyMusicPlayer.m文件中还需要实现一个AVAudioPlayerDelegate协议中约定的方法:audioPlayerDidFinishPlaying:successfully:方法,这个方法在AVAudioPlayer播放结束后会被调用,实现方法如下: -(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{ _player = nil; _isPlaying=NO; //是否循环播放 if (_isRandom) { unsigned long max = self.songsArray.count; int songIndex = arc4random()%max; NSString * path = [[NSBundle mainBundle]pathForResource:[self.songsArray objectAtIndex:songIndex] ofType:@"mp3"]; NSURL * url = [NSURL fileURLWithPath:path]; _player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil]; _player.delegate=self; [_player play]; _isPlaying=YES; [self.delegate musicPlayEndAndWillContinuePlaying:YES]; _currentIndex=songIndex; _currentSongTime=_player.duration; return; } if (_currentIndex @end 在MusicContentView.m文件中声明如下属性: @implementation MusicContentView { UIScrollView * _scrollView; //歌曲列表格视图 UITableView * _titleTableView; //单行显示的歌词显示标签 UILabel * _lrcLabel; //锁屏图片中的歌词标签 UILabel * _lrcIMGLabel; //锁屏图片的背景 UIImageView * _lrcIMGbg; //多行显示的歌词显示标签 UILabel * _lrcView; //多行显示歌词视图的显示行数 int _lines; } @end 在MusicContentView.m文件中实现类的初始化方法,如下所示: - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { //设置视图背景为透明色 self.backgroundColor = [UIColor clearColor]; //初始化滚动视图 _scrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; [self addSubview:_scrollView]; _scrollView.backgroundColor = [UIColor clearColor]; //初始化歌曲列表 _titleTableView = [[UITableView alloc]initWithFrame:CGRectMake(40,0, frame.size.width-90, frame.size.height-40) style:UITableViewStylePlain]; _titleTableView.backgroundColor = [UIColor clearColor]; _titleTableView.delegate=self; _titleTableView.dataSource=self; //设置表格视图行间无分割线 _titleTableView.separatorStyle = UITableViewCellSeparatorStyleNone; [_scrollView addSubview:_titleTableView]; //设置滚地视图的可滚动范围 _scrollView.contentSize = CGSizeMake(frame.size.width*2, frame.size.height); _scrollView.showsHorizontalScrollIndicator=NO; //设置滚动视图翻页效果 _scrollView.pagingEnabled=YES; //初始化单行显示的歌词控件 _lrcLabel = [[UILabel alloc]initWithFrame:CGRectMake(20, frame.size.height-50, frame.size.width-40, 50)]; _lrcLabel.backgroundColor = [UIColor clearColor]; //设置歌词颜色为白色 _lrcLabel.textColor = [UIColor whiteColor]; [_scrollView addSubview:_lrcLabel]; _lrcLabel.textAlignment = NSTextAlignmentCenter; _lrcLabel.numberOfLines=0; //初始化多行显示的歌词控件 _lrcView = [[UILabel alloc]initWithFrame:CGRectMake(frame.size.width+20, 50, frame.size.width-40, frame.size.height-100)]; //根据屏幕尺寸获取显示行数 _lines = (int)_lrcView.frame.size.height/21; _lrcView.numberOfLines = _lines; _lrcView.textAlignment = NSTextAlignmentCenter; _lrcView.textColor = [UIColor whiteColor]; [_scrollView addSubview:_lrcView]; //初始化锁屏图片上的歌词标签 _lrcIMGLabel = [[UILabel alloc]initWithFrame:CGRectMake(20, 0, self.frame.size.width-40, self.frame.size.height)]; _lrcIMGLabel.numberOfLines = _lines; _lrcIMGLabel.textAlignment = NSTextAlignmentCenter; _lrcIMGLabel.textColor = [UIColor whiteColor]; } return self; } 在MusicContentView.m中重写实现titleDataArray数据源的setter方法,如下所示: -(void)setTitleDataAttay:(NSArray *)titleDataAttay{ _titleDataAttay = [NSArray arrayWithArray:titleDataAttay]; [_titleTableView reloadData]; } 在MusicContentView.m中实现UITableView控件的代理与数据源协议中的相应方法如下所示: -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ //设置分区数为1 return 1; } -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ //设置行数为数据源中数据个数 return self.titleDataAttay.count; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cellId"]; if (cell==nil) { cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cellId"]; cell.backgroundColor = [UIColor clearColor]; cell.textLabel.textColor = [UIColor whiteColor]; //设置cell的选中效果为无 cell.selectionStyle = UITableViewCellSelectionStyleNone; } cell.textLabel.text = self.titleDataAttay[indexPath.row]; return cell; } -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ //单击歌曲列表中某行后播放相应的歌曲 [self.play playAtIndex:(int)indexPath.row isPlay:self.play.isPlaying]; } 在MusicContentView.m中实现setCurrentLRCArray:index:方法如下所示: -(void)setCurretLRCArray:(NSArray *)array index:(int)index{ NSString * lineLRC = [(LRCItem *)array[index] lrc]; _lrcLabel.text = lineLRC; //进行行数设置 NSMutableString * lrcStr = [[NSMutableString alloc]init]; if (index<_lines/2) { //前面用\n补齐 int offset = (int)_lines/2-index; for (int j=0; j 在ViewController.m中编写遵守相关协议的代码并声明一些属性如下所示: @interface ViewController () { MyMusicPlayer * _player; //内容视图 MusicContentView * _contentView; //标题标签 UILabel * _titleLabel; //进度条 UIProgressView * _progress; //播放按钮 UIButton * _playBtn; //下一曲按钮 UIButton * _nextBtn; //上一曲按钮 UIButton * _lastBtn; //循环播放按钮 UIButton * _circleBtn; //随机播放按钮 UIButton * _randomBtn; //存放歌曲名 NSArray * _dataArray; NSTimer * _timer; } @end 在ViewController.m文件的viewDidLoad方法中调用如下方法: - (void)viewDidLoad { [super viewDidLoad]; //创建数据 [self creatData]; //创建播放模块 [self creatPlayer]; //创建视图模块 [self creatView]; //进行刷新UI操作 [self updateUI]; } creatData方法的实现如下所示: -(void)creatData{ _dataArray = @[@"匆匆那年",@"致青春",@"清风徐来",@"矜持",@"暗涌",@"天空",@"容易受伤的女人",@"清平调",@"但愿人长久",@"暧昧",@"执迷不悔",@"约定",@"我愿意",@"棋子",@"梦醒了",@"影子",@"人间",@"爱与痛的边缘",@"旋木",@"红豆",@"传奇",@"爱不可及"]; } creatData方法对数据源进行了创建,前提是要将上面数组中歌名对应的音频文件和歌词文件都导入项目中,歌词文件要与音频文件名称对应,为了使工程结构看起来更整洁,开发者可以将歌曲与歌词文件分别放于相应的目录下,如图5-33所示。 图5-33 Xcode的工程目录结构 creatPlayer方法的实现如下: -(void)creatPlayer{ _player = [[MyMusicPlayer alloc]init]; _player.songsArray=_dataArray; NSMutableArray * mulArr = [[NSMutableArray alloc]init]; for (int i=0; i<_dataArray.count; i++) { //进行歌词模块创建 LRCEngine * engine = [[LRCEngine alloc]initWithFile:_dataArray[i]]; [mulArr addObject:engine]; } _player.lrcsArray = mulArr; _player.delegate=self; } creatView方法的实现如下: -(void)creatView{ //创建背景 UIImageView * bg = [[UIImageView alloc]initWithFrame:self.view.bounds]; bg.image = [UIImage imageNamed:@"BG.jpeg"]; //设置为可接收用户交互 bg.userInteractionEnabled=YES; [self.view addSubview:bg]; //创建歌曲标题Label _titleLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 20, bg.frame.size.width, 40)]; _titleLabel.font = [UIFont boldSystemFontOfSize:22]; _titleLabel.textAlignment = NSTextAlignmentCenter; _titleLabel.text = _dataArray[0]; _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.textColor = [UIColor whiteColor]; [bg addSubview:_titleLabel]; //创建歌曲进度条 _progress = [[UIProgressView alloc]initWithProgressViewStyle:UIProgressViewStyleDefault]; _progress.progressTintColor=[UIColor redColor]; _progress.trackTintColor = [UIColor whiteColor]; _progress.frame=CGRectMake(20, self.view.frame.size.height-70, self.view.frame.size.width-40, 5); [bg addSubview:_progress]; //创建播放按钮 _playBtn = [UIButton buttonWithType:UIButtonTypeCustom]; [_playBtn setBackgroundImage:[UIImage imageNamed:@"play"] forState:UIControlStateNormal]; [_playBtn setBackgroundImage:[UIImage imageNamed:@"pause"] forState:UIControlStateSelected]; _playBtn.frame=CGRectMake(self.view.frame.size.width/2-20, self.view.frame.size.height-45, 40, 30); [_playBtn addTarget:self action:@selector(playMusic) forControlEvents:UIControlEventTouchUpInside]; [bg addSubview:_playBtn]; //创建下一曲按钮 _nextBtn = [UIButton buttonWithType:UIButtonTypeCustom]; _nextBtn.frame=CGRectMake(self.view.frame.size.width/2+40, self.view.frame.size.height-45, 40, 30); [_nextBtn setBackgroundImage:[UIImage imageNamed:@"nextMusic"] forState:UIControlStateNormal]; [_nextBtn addTarget:self action:@selector(next) forControlEvents:UIControlEventTouchUpInside]; [bg addSubview:_nextBtn]; //创建上一曲按钮 _lastBtn = [UIButton buttonWithType:UIButtonTypeCustom]; _lastBtn.frame = CGRectMake(self.view.frame.size.width/2-80, self.view.frame.size.height-45, 40, 30); [_lastBtn setBackgroundImage:[UIImage imageNamed:@"aboveMusic"] forState:UIControlStateNormal]; [_lastBtn addTarget:self action:@selector(last) forControlEvents:UIControlEventTouchUpInside]; [bg addSubview:_lastBtn]; //创建循环播放按钮 _circleBtn = [UIButton buttonWithType:UIButtonTypeCustom]; _circleBtn.frame = CGRectMake(self.view.frame.size.width/2-140, self.view.frame.size.height-45, 40, 30); [_circleBtn setBackgroundImage:[UIImage imageNamed:@"circleClose"] forState:UIControlStateNormal]; [_circleBtn setBackgroundImage:[UIImage imageNamed:@"circleOpen"] forState:UIControlStateSelected]; [_circleBtn addTarget:self action:@selector(circle) forControlEvents:UIControlEventTouchUpInside]; [bg addSubview:_circleBtn]; //创建随机播放按钮 _randomBtn = [UIButton buttonWithType:UIButtonTypeCustom]; _randomBtn.frame=CGRectMake(self.view.frame.size.width/2+100, self.view.frame.size.height-45, 40, 30); [_randomBtn setBackgroundImage:[UIImage imageNamed:@"randomClose"] forState:UIControlStateNormal]; [_randomBtn setBackgroundImage:[UIImage imageNamed:@"randomOpen"] forState:UIControlStateSelected]; [_randomBtn addTarget:self action:@selector(random) forControlEvents:UIControlEventTouchUpInside]; [bg addSubview:_randomBtn]; //创建歌曲列表与歌词显示控件视图 _contentView = [[MusicContentView alloc]initWithFrame:CGRectMake(0, 70, self.view.frame.size.width, self.view.frame.size.height-150)]; _contentView.titleDataAttay = _dataArray; _contentView.play=_player; [bg addSubview:_contentView]; } 各个功能按钮的触发方法的实现如下: -(void)playMusic{ if (_player.isPlaying) { _playBtn.selected=NO; [_player stop]; }else{ _playBtn.selected=YES; [_player play]; } } -(void)next{ [_player nextMusic]; } -(void)last{ [_player lastMusic]; } -(void)circle{ if (_player.isRunLoop) { _player.isRunLoop=NO; _circleBtn.selected=NO; }else{ _player.isRunLoop=YES; _circleBtn.selected=YES; } } -(void)random{ if (_player.isRandom) { _player.isRandom=NO; _randomBtn.selected=NO; }else{ _player.isRandom=YES; _randomBtn.selected=YES; } } updateUI方法的实现如下: -(void)updateUI{ _timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0 target:self selector:@selector(update) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; } 定时器的触发方法update的实现如下: -(void)update{ _titleLabel.text = _dataArray[[_player currentIndex]]; //更新进度条 if (_player.hadPlayTime!=0) { float progress = (float)_player.hadPlayTime/_player.currentSongTime; _progress.progress = progress; } //更新歌词 LRCEngine * engine = _player.lrcsArray[_player.currentIndex]; [engine getCurrentLRCInLRCArray:^(NSArray *lrcArray, int currentIndex) { [_contentView setCurretLRCArray:lrcArray index:currentIndex]; } atTime:_player.hadPlayTime]; //更新锁屏界面 NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; [dict setObject:_dataArray[_player.currentIndex] forKey:MPMediaItemPropertyTitle]; [dict setObject:@"王菲" forKey:MPMediaItemPropertyArtist]; [dict setObject:@"致敬天后" forKey:MPMediaItemPropertyAlbumTitle]; UIImage *newImage = _contentView.lrcImage; [dict setObject:[[MPMediaItemArtwork alloc] initWithImage:newImage] forKey:MPMediaItemPropertyArtwork]; [dict setObject:[NSNumber numberWithDouble:_player.currentSongTime] forKey:MPMediaItemPropertyPlaybackDuration]; [dict setObject:[NSNumber numberWithDouble:_player.hadPlayTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //音乐当前已经过时间 [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict]; } 在ViewController.m中还需要实现一首歌曲播放完毕后调用的协议方法,如下所示: -(void)musicPlayEndAndWillContinuePlaying:(BOOL)play{ if (play) { _playBtn.selected=YES; }else{ _playBtn.selected=NO; } } 5.5.6 后台播放音频用户交互的处理 后台播放音频的用户交互是通过系统与AppDalegate类对象实现的,在AppDelegate.m文件的application:didFinishLaunchingWithOptions:方法中添加如下代码: - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { AVAudioSession *session = [AVAudioSession sharedInstance]; [session setActive:YES error:nil]; [session setCategory:AVAudioSessionCategoryPlayback error:nil]; [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; return YES; } 在AppDelegate.m中实现接收交互通知的方法如下: //后台播放控制 -(void)remoteControlReceivedWithEvent:(UIEvent *)event{ if (event.type==UIEventTypeRemoteControl) { switch (event.subtype) { case UIEventSubtypeRemoteControlPlay: [self.play play]; break; case UIEventSubtypeRemoteControlNextTrack: [self.play nextMusic]; break; case UIEventSubtypeRemoteControlPreviousTrack: [self.play lastMusic]; break; case UIEventSubtypeRemoteControlPause: [self.play stop]; break; case UIEventSubtypeRemoteControlTogglePlayPause: [self.play playOrStop]; break; default: break; } } } 至此,《天后王菲》音频播放器应用就已经开发完成了,其中主要界面如图5-34~图5-37所示。 图5-34 歌曲列表界面 图5-35 多行歌词显示界面 图5-36 后台播放上拉抽屉界面 图5-37 后台播放锁屏界面 学习之余,读者可以使用这款小应用听听音乐,放松一下,本章最后,向歌坛天后王菲致以敬意