QQ音乐项目共分为界面布局、切歌与播放、歌词显示、滑动歌词界面布局和播放、后台播放、锁屏界面、远程控制事件、打断处理8个功能。主要是复习原来学习的知识和将新知识用到实际项目中来。项目功能和牵扯的知识点还是比较多,大家应多复习一下。
================================================================================================
#pragma mark 1.界面布局
1.1界面布局用到sizeClass,相信大家已经学习过并能熟练使用。布局时候注意在横竖屏都有的界面需要在storyBoard选择紧凑和宽松状态下都是any的情况。
1.2在使用自动布局时,要遵循Auto layout的线性公式(一个控件的位置=另一个控件的位置 *一个系数+一个常量),每一个控件都要确定x,y,width,height4个属性,项目中的label和button不设置宽高,系统默认按照它们的内容给一个合适的宽高。
1.3代码自动布局使用第三方框架Masonry,给背景图片添加毛玻璃效果利用系统UIToolBar自带的效果,记得设置toolBar的style为black,代码:UIToolbar *toolBar = [[UIToolbar alloc] init];
toolBar.barStyle = UIBarStyleBlack;
1.4剪切正方形图片为圆形,设置它图层的cornerRadius为它宽的一半即可,在viewDidLoad如果显示不是圆形,需要程序员手动强制布局:[self.view layoutIfNeeded];
================================================================================================
#pragma mark 2.切歌与播放
2.1切歌
切歌本质上是取出当前的数据模型,然后给对应控件赋值。当索引为模型数组的最后一个时,下一曲就是数组的第0个模型,同理当索引为模型数组的第0个时,上一曲就是数组的最后一个模型。
2.2播放
由于系统只可能同时播放一个音乐文件,为了节省内存消耗,可以考虑将播放音乐封装成一个单例类,用来专门管理音乐播放暂停以及播放进度。在播放音乐时,记得切换播放按钮的selected状态,根据状态来决定是暂停还是播放。
2.3自动下一曲
自动下一曲采用block回调的方式实现。
2.3.1首先创建一个block,并在播放歌曲的方法中赋值
2.3.2设置音乐播放器的代理,在audioPlayerDidFinishPlaying: successfully:代理方法中调用block。
注意:调用block的代码是self.complete()如果不小心写成[self complete]系统不会报错,但也不会执行方法。
================================================================================================
#pragma mark 3.歌词显示
3.1加载歌词
3.1.1首先加载出本地的歌词,用UTF8解码
NSString *lyricStr = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
3.1.2为了匹配每一句的歌词,用换行符号得到每一行的歌词: NSArray *lineStrs = [lyricStr componentsSeparatedByString:@"\n"];
3.1.3遍历歌词数组,用正则表达式来匹配每行歌词的开始时间,正则表达式:@"\\[[0-9]{2}:[0-9]{2}.[0-9]{2}\\]"
匹配后我们将得到每行歌词时间的位置range和歌词的内容。
//根据每行歌词最后一个匹配到的时间位置,来截取出歌词内容(lastRes是匹配到的歌词时间数组的最后一个)
NSString *content = [lineStr substringFromIndex:lastRes.range.location + lastRes.range.length];
3.1.4遍历歌词时间数组,用NSDateFormatter将时间转换为date格式,并与00:00.00比较,即可得到"每行"歌词的"开始时间"
3.1.5将歌词的时间和内容赋值给模型,并将模型数组按照歌词时间按升序排序
3.2显示歌词和图片旋转
3.2.1在控制器得到歌词模型数组后根据当前播放时间和歌词的开始时间来选择最合适的歌词,基本判断逻辑是:当前的播放时间如果"小于"歌词模型的时间(注意是歌词的开始时间),就取出"上"一句歌词继续判断,如果当前的播放时间"大于"下一句歌词的播放时间,就取出"下"一句歌词继续判断.注意判断当前索引和数组长度的关系,防止数组越界.
if (pm.currentTime < lyric.time &&self.currentLyricIndex !=0) {
self.currentLyricIndex --;
[self updateLyric];
}elseif(pm.currentTime >= nextLyric.time &&self.currentLyricIndex != self.lyricArray.count -1){
self.currentLyricIndex ++;
[self updateLyric];
}
注意:当歌词显示完毕但歌曲仍然在播放时,即当前索引等于歌词数组的长度,为了防止数组越界,应该对下一句歌词的时间进行处理(让下句歌词的时间等于歌曲的总时间)
if (self.currentLyricIndex !=self.lyricArray.count - 1) {
nextLyric = self.lyricArray[self.currentLyricIndex +1];
}else{
nextLyric = [[CZLyric alloc] init];
nextLyric.time = musicTool.duration;
}
最后给歌词的label赋值,如果label是一个数组,用KVC可实现
[self.lyricLabel setValue:lyric.content forKey:@"text"];
3.3歌词变色
自定义一个label,在label的drawRect:(CGRect)rect方法中根据当前每行歌词的进度来决定歌词变色的宽度。
每行歌词的进度计算公式:(当前播放时间-这行歌词的开始时间) / (下句歌词的开始时间-当前歌词的开始时间)
在给label赋值当前歌词进度时,需要重绘label更新它的状态:[self setNeedsDisplay];
当前播放时间直接去音乐播放器的currentTime属性即可,在给当前时间的label持续赋值时,应改为NSString类型
3.4图片旋转
图片旋转使用tansform属性:
self.vCenterImageView.transform = CGAffineTransformRotate(self.vCenterImageView.transform, M_PI_2 *0.01);
需要持续更新,放在定时器的方法中。
3.5拖动进度条来选择歌曲播放时间和歌词
3.5.1根据播放时间移动进度条
改变UISlider的value属性值就可以改变进度条的移动,这里的value在最左边值为0,最右边值为1.-->所以value的算法是:当前播放时间/歌曲的总时间
3.5.2拖动进度条改变歌曲播放时间
拖动进度条时,根据进度条的value可以算出拖动位置歌曲的播放时间算法是:当前的播放时间= UISlider的value *歌曲的总时间
当改变音乐播放器的当前播放时间,那么在歌词处理代码中走原来的判断逻辑,就能找到正确的歌词
================================================================================================
#pragma mark 4.滑动歌词界面布局和播放
4.1基本布局
滑动界面需要自定义UIView,在自定义view中添加一个UIScrollView作为"横向"滑动,在"横向"滑动的UIScrollView上再添加一个"竖直"方向滑动的歌词界面
注意:在将UIScrollView布局时,不能参考另一个UIScrollView。而应该参考一个不变的控件,如self.bounds.size.width
4.2显示歌词
4.2.1设置滑动范围
在自定义view提供歌词数组lyrics,根据数组长度和每行label的高度设置滑动范围:self.vScrollView.contentSize = CGSizeMake(0, self.rowHeight * lyrics.count);
4.2.2显示歌词
>>>>显示基本歌词
在遍历数组方法中添加显示歌词的自定义label,设置基本属性(字体大小,歌词内容等).注意在切换歌词时应该将创建的label移除(removeFromSuperview)
>>>>根据播放进度使歌词自动滚动
自动滚动本质上是改变UIScrollView的contentOffset属性算法:竖直滚动的距离 =当前歌词索引 *行高 - UIScrollView上面的留白高度
>>>>改变当前播放歌词的字体大小
根据当前歌词索引能从UIScrollView的子控件数组中取出label,改变label的字体大小即可.
CZColorLabel *colorLabel = self.vScrollView.subviews[currentIndex];
colorLabel.font = [UIFont systemFontOfSize:20];
注意:当不是当前播放歌词时,应将字体大小再调小
>>>>改变当前播放歌词的颜色
和主界面改变歌词颜色相同,在根据当前播放歌词索引得到label后给label的进度(progress)赋值即可。
注意点:当播放过去后也需要将progress赋值为0(颜色改变回来)
4.3滑动歌词界面显示从当前位置播放条
4.3.1自定义播放条
继承自UIView,设置各个控件的属性和布局。
4.3.2播放条的显示和隐藏
>>>>在UIScrollView开始拖拽的代理方法中显示(scrollViewWillBeginDragging:)
>>>>在UIScrollView结束拖拽的代理方法中隐藏(scrollViewWillEndDragging)
注意:为了让用户可以点击播放按钮,需要用GCD做延时隐藏。但需要判断用户是否仍在拖拽中,如果拖拽中,就不隐藏:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!self.vScrollView.isDragging) {
self.sliderView.hidden =YES;
}
});
4.3.3显示当前位置时间和播放
>>>>在拖动时根据拖动位置得到拖动位置的索引,并根据索引取出歌词模型得到这行歌词的开始时间,代码如下:
CGFloat offset = self.vScrollView.contentOffset.y +self.vScrollView.contentInset.top;
NSInteger lyricIndex = offset / self.rowHeight;
if(lyricIndex <0){
lyricIndex = 0;
}elseif(lyricIndex > self.lyrics.count -1)
{
lyricIndex = self.lyrics.count -1;
}
CZLyric *lyric = self.lyrics[lyricIndex];
self.sliderView.time = lyric.time;
>>>>从当前拖动位置播放
根据当前拖动位置的歌词开始时间,直接赋值给音乐播放器的currentTime属性即可
================================================================================================
#pragma mark 5.后台播放
后台播放在模拟器上是可以的。但在真机中需要2个步骤
5.1开启backGround modes
在项目中的-->target-->capabilities-->backGround modes中状态选为ON,并勾选第一个Audio,AirPlay and Picture in Picture
5.2真机注册
在AppDelegate中的didFinishLaunchingWithOptions方法中添加以下代码
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
================================================================================================
#pragma mark 6.锁屏界面
6.1锁屏界面基本显示
首先在AppDelegate中的didFinishLaunchingWithOptions方法中添加以下代码:
[application beginReceivingRemoteControlEvents];
当锁屏时,依然可以显示当前播放歌曲的信息.使用MediaPlayer框架中的MPNowPlayingInfoCenter类来设置。创建一个字典赋值给MPNowPlayingInfoCenter的nowPlayingInfo属性。在字典中的key为下面的值。基本解释如下:
// MPMediaItemPropertyAlbumTitle 专辑名
// MPMediaItemPropertyArtist 歌手名
// MPMediaItemPropertyArtwork 专辑图片
// MPMediaItemPropertyPlaybackDuration 歌曲总时间
// MPMediaItemPropertyTitle 歌曲名称
// MPNowPlayingInfoPropertyElapsedPlaybackTime 歌曲播放进度
6.2锁屏界面歌词显示
锁屏界面的歌词本质上是一张不停变化的图片,利用Quartz2D来绘制图片并显示。
>>>>开始图形上下文
UIGraphicsBeginImageContext(CGSizeMake(bgImageW, bgImageH));
>>>>绘制背景图片
[bgImage drawInRect:CGRectMake(0,0, bgImageW, bgImageH)];
>>>>绘制歌词内容
[lyric.content drawInRect:CGRectMake(0, lyricBgImageY, lyricBgImageW, lyricBgImageH) withFont:[UIFont systemFontOfSize:16] lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentCenter];
>>>>从当前的图形上下文中取出图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
>>>>关闭图形上下文
UIGraphicsEndImageContext();
================================================================================================
#pragma mark 7.远程控制事件
远程控制事件指在锁屏界面下用户可以实现暂停、播放、上一曲和下一曲功能,在remoteControlReceivedWithEvent:方法中实现
iOS中的事件有三大类型,解释如下
//UIEventTypeTouches, 触摸事件(拖拽,捏合等)
//UIEventTypeMotion, 运动事件(摇一摇,加速计等)
//UIEventTypeRemoteControl, 远程控制事件
而在远程控制事件中又分为以下类型
/*
UIEventSubtypeRemoteControlPlay = 100,播放
UIEventSubtypeRemoteControlPause = 101,暂停
UIEventSubtypeRemoteControlStop = 102,停止
UIEventSubtypeRemoteControlTogglePlayPause = 103,从播放到暂停
UIEventSubtypeRemoteControlNextTrack = 104,下一首
UIEventSubtypeRemoteControlPreviousTrack = 105,上一首
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,开始快退
UIEventSubtypeRemoteControlEndSeekingBackward = 107,结束快退
UIEventSubtypeRemoteControlBeginSeekingForward = 108,开始快进
UIEventSubtypeRemoteControlEndSeekingForward = 109,结束快进
*/
只需在收到特定类型时调用对应的播放,上一曲或下一曲即可
================================================================================================
#pragma mark 8.打断处理
在播放音乐时如果用户接到电话等事件发生,为了保证用户体验,我们应先暂停播放,当挂断电话后再继续播放
在CZPlayManager方法中,我们接受系统自动调用的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionInterruptionNotification:) name:AVAudioSessionInterruptionNotification object:nil];
根据通知的值来做出相应的操作
AVAudioSessionInterruptionType type = [notification.userInfo[AVAudioSessionInterruptionTypeKey] intValue];
if (type == AVAudioSessionInterruptionTypeBegan) {
[self.player pause];
}else{
[self.player play];
}
================================================================================================
demo代码日后补充!!