輕仿QQ音樂之音頻歌詞播放、鎖屏歌詞

最近閑下來自己寫了個小demo,輕仿QQ音樂播放界面,本文主要講一下音頻的基本播放、歌詞的滾動對應(yīng)、鎖屏歌詞的實(shí)現(xiàn)(會持續(xù)更新音頻相關(guān)的知識點(diǎn)),老規(guī)矩,先上效果圖

歌詞播放界面
音樂播放界面
鎖屏歌詞界面

一. 項(xiàng)目概述

前面內(nèi)容實(shí)在是太基礎(chǔ)。。只想看知識點(diǎn)的同學(xué)可以直接跳到第三部分的干貨

/** 圖片 */
@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;

對應(yīng)plist存儲文件

音樂模型所對應(yīng)的 plist 存儲文件
  • 歌詞模型-->WPFLyric
/** 歌詞開始時間 */
@property (nonatomic,assign) NSTimeInterval time;

/** 歌詞內(nèi)容 */
@property (nonatomic,copy) NSString *content;
  • 歌詞展示界面-->WPFLyricView
@property (nonatomic,weak) id <WPFLyricViewDelegate> delegate;

/** 歌詞模型數(shù)組 */
@property (nonatomic,strong) NSArray *lyrics;

/** 每行歌詞行高 */
@property (nonatomic,assign) NSInteger rowHeight;

/** 當(dāng)前正在播放的歌詞索引 */
@property (nonatomic,assign) NSInteger currentLyricIndex;

/** 歌曲播放進(jìn)度 */
@property (nonatomic,assign) CGFloat lyricProgress;

/** 豎直滾動的view,即歌詞View */
@property (nonatomic,weak) UIScrollView *vScrollerView;

#warning 以下為私有屬性

/* 水平滾動的大view,包含音樂播放界面及歌詞界面 */
@property (nonatomic,weak) UIScrollView *hScrollerView;

/** 定位播放的View */
@property (nonatomic,weak) WPFSliderView *sliderView;
  • 當(dāng)前正在播放的歌詞label-->WPFColorLabel
/** 歌詞播放進(jìn)度 */
@property (nonatomic,assign) CGFloat progress;

/** 歌詞顏色 */
@property (nonatomic,strong) UIColor *currentColor;
  • 播放管理對象-->WPFPlayManager
/** 單例分享 */
+ (instancetype)sharedPlayManager;

/**
 *  播放音樂的方法
 *
 *  @param fileName 音樂文件的名稱
 *  @param complete 播放完畢后block回調(diào)
 */
- (void)playMusicWithFileName:(NSString *)fileName didComplete:(void(^)())complete;

/** 音樂暫停 */
- (void)pause;
  • 歌詞解析器-->WPFLyricParser (主要就是根據(jù) .lrc 文件解析歌詞的方法)
+ (NSArray *)parserLyricWithFileName:(NSString *)fileName {
    
    // 根據(jù)文件名稱獲取文件地址
    NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
    
    // 根據(jù)文件地址獲取轉(zhuǎn)化后的總體的字符串
    NSString *lyricStr = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
    
    // 將歌詞總體字符串按行拆分開,每句都作為一個數(shù)組元素存放到數(shù)組中
    NSArray *lineStrs = [lyricStr componentsSeparatedByString:@"\n"];
    
    // 設(shè)置歌詞時間正則表達(dá)式格式
    NSString *pattern = @"\\[[0-9]{2}:[0-9]{2}.[0-9]{2}\\]";
    NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
    
    // 創(chuàng)建可變數(shù)組存放歌詞模型
    NSMutableArray *lyrics = [NSMutableArray array];
    
    // 遍歷歌詞字符串?dāng)?shù)組
    for (NSString *lineStr in lineStrs) {
        
        NSArray *results = [reg matchesInString:lineStr options:0 range:NSMakeRange(0, lineStr.length)];
        
        // 歌詞內(nèi)容
        NSTextCheckingResult *lastResult = [results lastObject];
        NSString *content = [lineStr substringFromIndex:lastResult.range.location + lastResult.range.length];
        
        // 每一個結(jié)果的range
        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]"];
            
            // 創(chuàng)建模型
            WPFLyric *lyric = [[WPFLyric alloc] init];
            lyric.content = content;
            // 歌詞的開始時間
            lyric.time = [timeDate timeIntervalSinceDate:initDate];
            
            // 將歌詞對象添加到模型數(shù)組匯總
            [lyrics addObject:lyric];
        }
    }
    
    // 按照時間正序排序
    NSSortDescriptor *sortDes = [NSSortDescriptor sortDescriptorWithKey:@"time" ascending:YES];
    [lyrics sortUsingDescriptors:@[sortDes]];
   
    return lyrics;
}

二. 主要知識點(diǎn)講解

  • 音頻播放AppDelegate中操作
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 注冊后臺播放
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayback error:NULL];
    
    // 開啟遠(yuǎn)程事件  -->自動切歌
    [application beginReceivingRemoteControlEvents];
    
    return YES;
}
  • 音頻播放加載文件播放方式
NSURL *url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:NULL];
  • 在 ViewController 中點(diǎn)擊事件
#warning 播放/暫停按鈕點(diǎn)擊事件
- (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 下一曲按鈕點(diǎn)擊事件
- (IBAction)next {
    // 循環(huán)播放
    if (self.currentMusicIndex == self.musics.count -1) {
        self.currentMusicIndex = 0;
    }else{
        self.currentMusicIndex ++;
    }
    [self changeMusic];
}

#warning changeMusic 方法
// 重置音樂對象,各種基礎(chǔ)賦值
- (void)changeMusic {
    // 防止切歌時歌詞數(shù)組越界
    
    self.currentLyricIndex = 0;
    // 切歌時銷毀當(dāng)前的定時器
    [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];
}

三. 鎖屏歌詞詳細(xì)講解

  • 更新鎖屏界面的方法最好在一句歌詞唱完之后的方法中調(diào)用(還是結(jié)合代碼添加注釋吧,干講... 臣妾做不到啊)
- (void)updateLockScreen {
#warning 鎖屏界面的一切信息都要通過這個原生的類來創(chuàng)建:MPNowPlayingInfoCenter
    // 獲取音樂播放信息中心
    MPNowPlayingInfoCenter *nowPlayingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
    // 創(chuàng)建可變字典存放信息
    NSMutableDictionary *info = [NSMutableDictionary dictionary];
    // 獲取當(dāng)前正在播放的音樂對象
    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]];
    // 當(dāng)前播放進(jìn)度
    info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(playManager.currentTime);
    // 音樂總時間
    info[MPMediaItemPropertyPlaybackDuration] = @(playManager.duration);
    // 音樂名稱
    info[MPMediaItemPropertyTitle] = music.name;
    
    nowPlayingInfoCenter.nowPlayingInfo = info;
}
  • 更新鎖屏歌詞的原理就是獲取專輯圖片后,將前后三句歌詞渲染到圖片上,使用富媒體將當(dāng)前正在播放的歌詞和前后的歌詞區(qū)分開大小和顏色
- (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];
    
    // 創(chuàng)建ImageView
    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();
    
    // 結(jié)束上下文
    UIGraphicsEndImageContext();
    
    return img;
}
  • 當(dāng)然不是所有的時候都要去更新鎖屏多媒體信息的,可以采用下面的方法進(jìn)行監(jiān)聽優(yōu)化:只在鎖屏而且屏幕亮著的時候才會去設(shè)置,啥都不說了,都在代碼里了
#warning 聲明的全局變量及通知名稱
static uint64_t isScreenBright;
static uint64_t isLocked;
#define kSetLockScreenLrcNoti @"kSetLockScreenLrcNoti"

#warning 在 viewDidLoad 方法中監(jiān)聽
    // 監(jiān)聽鎖屏狀態(tài)
    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];
  • 上面兩個監(jiān)聽對應(yīng)的方法:
// 監(jiān)聽在鎖定狀態(tài)下,屏幕是黑暗狀態(tài)還是明亮狀態(tài)
static void updateEnabled(CFNotificationCenterRef center, void* observer, CFStringRef name, const void* object, CFDictionaryRef userInfo) {
    
    //    uint64_t state;
    
    int token;
    
    notify_register_check("com.apple.iokit.hid.displayStatus", &token);
    
    notify_get_state(token, &isScreenBright);
    
    notify_cancel(token);
    
    [ViewController checkoutIfSetLrc];
    
    //    NSLog(@"鎖屏狀態(tài):%llu",isScreenBright);
}

// 監(jiān)聽屏幕是否被鎖定
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];
    //    NSLog(@"lockState狀態(tài):%llu",state);
}

#warning 這個方法不太好,有好想法的可在評論區(qū)討論
+ (void)checkoutIfSetLrc {
    // 如果當(dāng)前屏幕被鎖定 && 屏幕處于 active 狀態(tài),就發(fā)送通知調(diào)用對象方法
    if (isLocked && isScreenBright) {
        [[NSNotificationCenter defaultCenter] postNotificationName:kSetLockScreenLrcNoti object:nil];
    }
}

四. 后續(xù)干貨補(bǔ)充(不定時更新)

  • 當(dāng)前音頻被其他app音頻、照相機(jī)、鬧鐘、電話等打斷,打斷結(jié)束后立刻恢復(fù)播放
#warning AppDelegate中
// 指明應(yīng)用啟動原因的dictionary。如果用戶直接啟動應(yīng)用的話,dictionary為nil。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
      // 監(jiān)聽音頻被打斷的通知
      [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(interruptionNotificationCallback:) name:AVAudioSessionInterruptionNotification object:nil];
}

// 音頻被打斷后響應(yīng)的方法
- (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) {
        // 由于音頻被打斷的狀態(tài)改變后才會發(fā)送通知,因此需要在項(xiàng)目中記錄用戶最后一次手動操作的狀態(tài)(暫停/播放),在此處獲取,如果記錄的用戶最后一次操作是播放,那么就繼續(xù)播放
        if ([[NSUserDefaults standardUserDefaults] boolForKey:@"kUserControlPlayState"]) {
            #warning 在這里調(diào)用項(xiàng)目中繼續(xù)播放音頻的方法哦
        }
    }
}
  • 禁止鎖屏

默認(rèn)情況下,當(dāng)設(shè)備一段時間沒有觸控動作時,iOS會鎖住屏幕。但有一些情況是不需要鎖屏的,比如視頻播放器,或者播放歌詞界面的音樂播放器

[UIApplication sharedApplication].idleTimerDisabled = YES;
or
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];

最后再附一下GitHub地址吧,歡迎Star

千萬別打賞??!點(diǎn)個贊就好??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評論 25 709
  • 簡介 簡單來說,音頻可以分為2種 音效 又稱“短音頻”,通常在程序中的播放時長為1~2秒 在應(yīng)用程序中起到點(diǎn)綴效果...
    JonesCxy閱讀 1,009評論 1 2
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,036評論 4 61
  • 人,所謂之性,其私也,其利也,望其社會,所貪所欲之人,皆仕途高升,展其不求名利之人,皆窮困潦倒。謂其此,所堅(jiān)持之...
    f3149cceebc6閱讀 168評論 0 0
  • 自從去年和自己很親的姑姑去世以后,現(xiàn)年35歲的趙先生便患上了“恐癌癥”。 姑姑是得了胃癌走的,從檢查出來到離開前后...
    小新愛生活閱讀 323評論 0 0

友情鏈接更多精彩內(nèi)容