正文
2016.09.04
最近闲下来自己写了个小demo,轻仿QQ音乐播放界面,本文主要讲一下音频的基本播放、歌词的滚动对应、锁屏歌词的实现(会持续更新音频相关的知识点),老规矩,先上效果图
一. 项目概述
前面内容实在是太基础。。只想看知识点的同学可以直接跳到第三部分的干货
- 项目播放的mp3文件及lrc文件均来自QQ音乐
- 本文主要主要讲解锁屏歌词的实现,音频、歌词的播放网上资源略多,因此不做重点讲解,项目也是采取最简单的MVC+storyboard方式
- 项目GitHub地址: 感兴趣的同学可以下载下来结合该博文体验效果更佳
- 音乐模型-->WPFMusic
@property (nonatomic,copy) NSString *image;
@property (nonatomic,copy) NSString *lrc;
@property (nonatomic,copy) NSString *mp3;
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *singer;
@property (nonatomic,copy) NSString *album;
@property (nonatomic,assign) WPFMusicType type;
对应plist存储文件
@property (nonatomic,assign) NSTimeInterval time;
@property (nonatomic,copy) NSString *content;
@property (nonatomic,weak) id <WPFLyricViewDelegate> delegate;
@property (nonatomic,strong) NSArray *lyrics;
@property (nonatomic,assign) NSInteger rowHeight;
@property (nonatomic,assign) NSInteger currentLyricIndex;
@property (nonatomic,assign) CGFloat lyricProgress;
@property (nonatomic,weak) UIScrollView *vScrollerView;
#warning 以下为私有属性
@property (nonatomic,weak) UIScrollView *hScrollerView;
@property (nonatomic,weak) WPFSliderView *sliderView;
- 当前正在播放的歌词label-->WPFColorLabel
@property (nonatomic,assign) CGFloat progress;
@property (nonatomic,strong) UIColor *currentColor;
+ (instancetype)sharedPlayManager;
- (void)playMusicWithFileName:(NSString *)fileName didComplete:(void(^)())complete;
- (void)pause;
- 歌词解析器-->WPFLyricParser (主要就是根据 .lrc 文件解析歌词的方法)
+ (NSArray *)parserLyricWithFileName:(NSString *)fileName {
NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
NSString *lyricStr = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
NSArray *lineStrs = [lyricStr componentsSeparatedByString:@"\n"];
NSString *pattern = @"\\[[0-9]{2}:[0-9]{2}.[0-9]{2}\\]";
NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
NSMutableArray *lyrics = [NSMutableArray array];
for (NSString *lineStr in lineStrs) {
NSArray *results = [reg matchesInString:lineStr options:0 range:NSMakeRange(0, lineStr.length)];
NSTextCheckingResult *lastResult = [results lastObject];
NSString *content = [lineStr substringFromIndex:lastResult.range.location + lastResult.range.length];
for (NSTextCheckingResult *result in results) {
NSString *time = [lineStr substringWithRange:result.range];
#warning 对于类似 NSDateFormatter 的重大开小对象,最好使用单例管理
NSDateFormatter *formatter = [NSDateFormatter sharedDateFormatter];
formatter.dateFormat = @"[mm:ss.SS]";
NSDate *timeDate = [formatter dateFromString:time];
NSDate *initDate = [formatter dateFromString:@"[00:00.00]"];
WPFLyric *lyric = [[WPFLyric alloc] init];
lyric.content = content;
lyric.time = [timeDate timeIntervalSinceDate:initDate];
[lyrics addObject:lyric];
}
}
NSSortDescriptor *sortDes = [NSSortDescriptor sortDescriptorWithKey:@"time" ascending:YES];
[lyrics sortUsingDescriptors:@[sortDes]];
return lyrics;
}
二. 主要知识点讲解
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:NULL];
[application beginReceivingRemoteControlEvents];
return YES;
}
NSURL *url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:NULL];
#warning 播放/暂停按钮点击事件
- (IBAction)play {
WPFPlayManager *playManager = [WPFPlayManager sharedPlayManager];
if (self.playBtn.selected == NO) {
[self startUpdateProgress];
WPFMusic *music = self.musics[self.currentMusicIndex];
[playManager playMusicWithFileName:music.mp3 didComplete:^{
[self next];
}];
self.playBtn.selected = YES;
}else{
self.playBtn.selected = NO;
[playManager pause];
[self stopUpdateProgress];
}
}
#warning 下一曲按钮点击事件
- (IBAction)next {
if (self.currentMusicIndex == self.musics.count -1) {
self.currentMusicIndex = 0;
}else{
self.currentMusicIndex ++;
}
[self changeMusic];
}
#warning changeMusic 方法
- (void)changeMusic {
self.currentLyricIndex = 0;
[self stopUpdateProgress];
WPFPlayManager *pm = [WPFPlayManager sharedPlayManager];
WPFMusic *music = self.musics[self.currentMusicIndex];
self.lyrics = [WPFLyricParser parserLyricWithFileName:music.lrc];
self.lyricView.lyrics = self.lyrics;
self.albumLabel.text = music.album;
self.singerLabel.text = [NSString stringWithFormat:@"— %@ —", music.singer];
UIImage *image = [UIImage imageNamed:music.image];
self.vCenterImageView.image = image;
self.bgImageView.image = image;
self.hCennterImageView.image = image;
self.playBtn.selected = NO;
self.navigationItem.title = music.name;
[self play];
self.durationLabel.text = [WPFTimeTool stringWithTime:pm.duration];
}
三. 锁屏歌词详细讲解
- 更新锁屏界面的方法最好在一句歌词唱完之后的方法中调用(还是结合代码添加注释吧,干讲... 臣妾做不到啊)
- (void)updateLockScreen {
#warning 锁屏界面的一切信息都要通过这个原生的类来创建:MPNowPlayingInfoCenter
MPNowPlayingInfoCenter *nowPlayingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
NSMutableDictionary *info = [NSMutableDictionary dictionary];
WPFMusic *music = self.musics[self.currentMusicIndex];
WPFPlayManager *playManager = [WPFPlayManager sharedPlayManager];
info[MPMediaItemPropertyAlbumTitle] = music.album;
info[MPMediaItemPropertyArtist] = music.singer;
info[MPMediaItemPropertyArtwork] = [[MPMediaItemArtwork alloc] initWithImage:[self lyricImage]];
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(playManager.currentTime);
info[MPMediaItemPropertyPlaybackDuration] = @(playManager.duration);
info[MPMediaItemPropertyTitle] = music.name;
nowPlayingInfoCenter.nowPlayingInfo = info;
}
- 更新锁屏歌词的原理就是获取专辑图片后,将前后三句歌词渲染到图片上,使用富媒体将当前正在播放的歌词和前后的歌词区分开大小和颜色
- (UIImage *)lyricImage {
WPFMusic *music = self.musics[self.currentMusicIndex];
WPFLyric *lyric = self.lyrics[self.currentLyricIndex];
WPFLyric *lastLyric = [[WPFLyric alloc] init];
WPFLyric *nextLyric = [[WPFLyric alloc] init];
if (self.currentLyricIndex > 0) {
lastLyric = self.lyrics[self.currentLyricIndex - 1];
if (!lastLyric.content.length && self.currentLyricIndex > 1) {
lastLyric = self.lyrics[self.currentLyricIndex - 2];
}
}
if (self.lyrics.count > self.currentLyricIndex + 1) {
nextLyric = self.lyrics[self.currentLyricIndex + 1];
if (!nextLyric.content.length && self.lyrics.count > self.currentLyricIndex + 2) {
nextLyric = self.lyrics[self.currentLyricIndex + 2];
}
}
UIImage *bgImage = [UIImage imageNamed:music.image];
UIImageView *imgView = [[UIImageView alloc] initWithImage:bgImage];
imgView.bounds = CGRectMake(0, 0, 640, 640);
imgView.contentMode = UIViewContentModeScaleAspectFill;
UIView *cover = [[UIView alloc] initWithFrame:imgView.bounds];
cover.backgroundColor = [UIColor colorWithWhite:0 alpha:0.3];
[imgView addSubview:cover];
UILabel *lyricLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 480, 620, 150)];
lyricLabel.textAlignment = NSTextAlignmentCenter;
lyricLabel.numberOfLines = 3;
NSString *lyricString = [NSString stringWithFormat:@"%@ \n%@ \n %@", lastLyric.content, lyric.content, nextLyric.content];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:lyricString attributes:@{
NSFontAttributeName : [UIFont systemFontOfSize:29],
NSForegroundColorAttributeName : [UIColor lightGrayColor]
}];
[attributedString addAttributes:@{
NSFontAttributeName : [UIFont systemFontOfSize:34],
NSForegroundColorAttributeName : [UIColor whiteColor]
} range:[lyricString rangeOfString:lyric.content]];
lyricLabel.attributedText = attributedString;
[imgView addSubview:lyricLabel];
UIGraphicsBeginImageContext(imgView.frame.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[imgView.layer renderInContext:context];
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
- 当然不是所有的时候都要去更新锁屏多媒体信息的,可以采用下面的方法进行监听优化:只在锁屏而且屏幕亮着的时候才会去设置,啥都不说了,都在代码里了
#warning 声明的全局变量及通知名称
static uint64_t isScreenBright;
static uint64_t isLocked;
#define kSetLockScreenLrcNoti @"kSetLockScreenLrcNoti"
#warning 在 viewDidLoad 方法中监听
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, updateEnabled, CFSTR("com.apple.iokit.hid.displayStatus"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, lockState, CFSTR("com.apple.springboard.lockstate"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
});
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateLockScreen) name:kSetLockScreenLrcNoti object:nil];
static void updateEnabled(CFNotificationCenterRef center, void* observer, CFStringRef name, const void* object, CFDictionaryRef userInfo) {
int token;
notify_register_check("com.apple.iokit.hid.displayStatus", &token);
notify_get_state(token, &isScreenBright);
notify_cancel(token);
[ViewController checkoutIfSetLrc];
}
static void lockState(CFNotificationCenterRef center, void* observer, CFStringRef name, const void* object, CFDictionaryRef userInfo) {
uint64_t state;
int token;
notify_register_check("com.apple.springboard.lockstate", &token);
notify_get_state(token, &state);
notify_cancel(token);
isLocked = state;
[ViewController checkoutIfSetLrc];
}
#warning 这个方法不太好,有好想法的可在评论区讨论
+ (void)checkoutIfSetLrc {
if (isLocked && isScreenBright) {
[[NSNotificationCenter defaultCenter] postNotificationName:kSetLockScreenLrcNoti object:nil];
}
}
四. 后续干货补充(不定时更新)
- 当前音频被其他app音频、照相机、闹钟、电话等打断,打断结束后立刻恢复播放
#warning AppDelegate中
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(interruptionNotificationCallback:) name:AVAudioSessionInterruptionNotification object:nil];
}
- (void)interruptionNotificationCallback:(NSNotification *)noti {
UInt32 optionKey = [noti.userInfo[AVAudioSessionInterruptionOptionKey] unsignedIntValue];
AudioSessionInterruptionType interruptionType = [noti.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntValue];
NSLog(@"optionKey-->%d", optionKey);
NSLog(@"interruptionType-->%d", interruptionType);
if (optionKey == 1 && interruptionType == 0) {
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"kUserControlPlayState"]) {
#warning 在这里调用项目中继续播放音频的方法哦
}
}
}
默认情况下,当设备一段时间没有触控动作时,iOS会锁住屏幕。但有一些情况是不需要锁屏的,比如视频播放器,或者播放歌词界面的音乐播放器
[UIApplication sharedApplication].idleTimerDisabled = YES;
or
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
最后再附一下GitHub地址吧,欢迎Star
千万别打赏!!点个赞就好😊