@(iOS Study)[音視頻播放]
- 作者: Liwx
- 郵箱: 1032282633@qq.com
目錄
- 02.音視頻播放 QQ音樂界面搭建 (自己整理)
- QQ音樂界面搭建
- 1.storyboard布局QQ音樂界面
- QQ音樂主界面整體框圖
- 界面控件設置
- 2.實現音樂的播放
- 封裝WXAudioTool播放音效和播放音樂工具類
- 播放音效/音樂工具類實現步驟
- 封裝WXMusicTool獲取音樂列表工具類
- 在主控制器實現播放音樂
- 3.添加播放進度定時器,更新播放進度
- 實現更新播放進度功能和歌手圖標旋轉功能
- 實時更新播放進度信息
- 歌手圖標旋轉動畫
- 4.處理滾動條
- 自定義滾動歌詞的WXLrcScrollView,繼承UIScrollView
- 5.播放/暫停,上一首,下一首功能實現,音樂播放完畢自動切換到下一首
- 播放/暫停,上一首,下一首功能實現
- 音樂播放完畢自動切換到下一首
- 6.設置進度條UISlider的處理
- 監聽進度條的事件
- 進度條添加Tap敲擊手勢
- 7.歌詞的解析
- 歌詞模型類功能的實現
- 創建歌詞文件處理WXLrcTool工具類
- 8.實現歌詞ScrollView歌詞的滾動功能
- 主界面控制器創建更新歌詞的定時器CADisplayLink定時器
- 自定義顯示歌詞的WXLrcLabel
- 實現歌詞的滾動,當前行歌詞進度顏色填充效果
- 10.設置主界面的歌詞
- 在主界面中設置主界面歌詞
- 由lrcScrollView內部為主界面歌詞內容和進度進行更新
- 11.實現鎖屏界面信息展示和操作
- 鎖屏界面項目配置
- 12.實現鎖屏鎖屏歌詞展示
- 繪制鎖屏封面和歌詞
- 鎖屏界面實現播放,暫停,上一首,下一首功能
- 1.storyboard布局QQ音樂界面
QQ音樂界面搭建
- QQ音樂運行效果(模擬器不能演示鎖屏界面的功能,所以展示效果圖沒有鎖屏界面功能展示)
1.storyboard布局QQ音樂界面
QQ音樂主界面整體框圖
- 界面效果圖
- UISlider添加手勢
- UILabel作為其他控件布局的參考控件注意點
- 如果UILabel的(頂部Top或底部Bottom)作為其他控件布局的參考對象的時候,需對設置UILabel的高度約束. 高度約束如下圖所示
界面控件設置
- 設置毛玻璃效果的幾種方式
- 美工做一張毛玻璃效果的圖片
- 使用UIToolbar
- 使用第三方框架
- 使用coreImage
- 使用UIVisualEffectView (Blur)
- 設置背景毛玻璃效果
- 本示例使用UIToolbar做毛玻璃效果
- 示例代碼
/** 給背景添加毛玻璃效果 */
- (void)setupBlur
{
// REMARKS: 添加毛玻璃效果
// 1.創建toolBar,設置毛玻璃樣式,添加到背景的view
UIToolbar *toolBar = [[UIToolbar alloc] init];
toolBar.barStyle = UIBarStyleBlack;
[self.albumView addSubview:toolBar];
// 2.toolBar添加約束
[toolBar mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.albumView);
}];
}
- 設置歌手圖標圓角效果
- 在view即將布局子控件的時候調用
viewWillLayoutSubviews方法
,該方法中可以獲取子控件的真實尺寸
- 在view即將布局子控件的時候調用
// SINGLE: view即將布局子控件的時候調用,在該方法中可以拿到子控件的真實尺寸
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
// SINGLE: 設置ImageView為圓形
self.iconView.layer.cornerRadius = self.iconView.frame.size.width * 0.5;
self.iconView.layer.masksToBounds = YES;
// SINGLE: 設置ImageView邊框顏色,寬度
self.iconView.layer.borderColor = [UIColor lightGrayColor].CGColor;
self.iconView.layer.borderWidth = 5.0;
}
- 設置UISlider滾動條
滑塊圖片
// SINGLE: 2.設置UISlider滑塊圖片
[self.progressSlider setThumbImage:[UIImage imageNamed:@"player_slider_playback_thumb"] forState:UIControlStateNormal];
2.實現音樂的播放
封裝WXAudioTool播放音效和播放音樂工具類
播放音效/音樂工具類實現步驟
- 1.創建一個可變字典用于存放
音樂播放器AVAudioPlayer
,創建一個可變字典用于存放播放音效SystemSoundID
,作為音樂播放器和音效播放的內存緩存.- 音樂播放器和音效播放器緩存只需創建一次,所以將其放在initialize方法中進行初始化操作.示例代碼如下
#pragma mark - 初始化設置
/** 創建內存緩存 */
+ (void)initialize {
// 創建可變字典,用于存放播放音效SystemSoundID
_soundIDs = [NSMutableDictionary dictionary];
// 創建可變字典,用于存放音樂播放器AVAudioPlayer
_players = [NSMutableDictionary dictionary];
}
-
2.WXAudioTool工具類實現播放等API接口方法
- 實現播放音樂,暫停音樂,停止音樂和播放音效API接口方法
播放音樂: + (AVAudioPlayer *)playMusicWithMusicName:(NSString *)musicName;
暫停音樂: + (void)pauseMusicWithMusicName:(NSString *)musicName;
停止音樂: + (void)stopMusicWithMusicName:(NSString *)musicName;
播放音效: + (void)playSoundWithSoundName:(NSString *)soundName; 3.WXAudioTool工具類實現代碼
#import "WXAudioTool.h"
#import <AVFoundation/AVFoundation.h>
@implementation WXAudioTool
// SINGLE: 創建一個可變字典緩存音樂,字典只需創建一次,可以在initialize類方法中創建
static NSMutableDictionary *_soundIDs;
// SINGLE: 創建音樂內存緩存,在initialize類方法中創建
static NSMutableDictionary *_players;
#pragma mark - 初始化設置
/** 創建內存緩存 */
+ (void)initialize {
// 創建可變字典,用于存放播放音效SystemSoundID
_soundIDs = [NSMutableDictionary dictionary];
// 創建可變字典,用于存放音樂播放器AVAudioPlayer
_players = [NSMutableDictionary dictionary];
}
#pragma mark - 播放音樂API(AVAudioPlayer)
/** 播放音樂 */
+ (AVAudioPlayer *)playMusicWithMusicName:(NSString *)musicName
{
// 1.先從內存字典中獲取播放器AVAudioPlayer
AVAudioPlayer *player = _players[musicName];
// 2.判斷是否從內存獲取到播放器,如果沒有獲取到,新建播放器
if (player == nil) {
// 2.1 獲取音樂文件的url
NSURL *url = [[NSBundle mainBundle] URLForResource:musicName withExtension:nil];
if (url == nil) return nil;
// 2.2 根據音頻文件的url,創建播放器
player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
// 2.3 保存到內存緩存_players
[_players setObject:player forKey:musicName];
}
// SINGLE: 3.播放音樂
[player play];
return player;
}
/** 暫停音樂 */
+ (void)pauseMusicWithMusicName:(NSString *)musicName
{
// 1.從內存字典中取出播放器
AVAudioPlayer *player = _players[musicName];
// 2.如果內存中有獲取到播放器,暫停播放
if (player) {
// SINGLE: 暫停播放
[player pause];
}
}
/** 停止音樂 */
+ (void)stopMusicWithMusicName:(NSString *)musicName
{
// 1.從內存字典中取出播放器
AVAudioPlayer *player = _players[musicName];
// 2.如果內存中有獲取到播放器,停止播放
if (player) {
// SINGLE: 2.1 停止播放
[player stop];
// 2.2 從內存字典中移除
[_players removeObjectForKey:musicName];
player = nil;
}
}
#pragma mark - 播放短音效API(SystemSoundID)
// REMARKS: 播放音效類方法
/** 播放音效 */
+ (void)playSoundWithSoundName:(NSString *)soundName
{
// 1.先從內存緩存獲取soundID
SystemSoundID soundID = [_soundIDs[soundName] unsignedIntValue];
// 2.判斷內存是否存在音效資源,內存沒有音效資源,則創建
if (soundID == 0) {
// 2.1 若不存在, 創建音效資源url
CFURLRef url = (__bridge CFURLRef)[[NSBundle mainBundle] URLForResource:soundName withExtension:nil];
// 2.2 判斷url是否為空,如果為空,說明資源不存在,直接退出
if (url == nil) return;
// 2.3 生成SystemSoundID
AudioServicesCreateSystemSoundID(url, &soundID);
// 2.4 存入可變字典內存緩存
[_soundIDs setObject:@(soundID) forKey:soundName];
}
// 3.播放音效
AudioServicesPlaySystemSound(soundID);
}
@end
封裝WXMusicTool獲取音樂列表工具類
- 1.創建WXMusicItem音樂模型
@interface WXMusicItem : NSObject
/** 音樂名 */
@property (nonatomic ,copy)NSString *name;
/** 音樂文件名 */
@property (nonatomic ,copy)NSString *filename;
/** 歌詞文件名 */
@property (nonatomic ,copy)NSString *lrcname;
/** 歌手名 */
@property (nonatomic ,copy)NSString *singer;
/** 歌手小圖標 */
@property (nonatomic ,copy)NSString *singerIcon;
/** 歌手大圖標 */
@property (nonatomic ,copy)NSString *icon;
@end
-
2.WXMusicTool工具類實現API接口方法
- 實現播放音樂,暫停音樂,停止音樂和播放音效API接口方法
獲取所有的音樂: + (NSArray *)musics;
獲取正在播放的音樂: + (WXMusicItem *)playingMusic;
設置播放的音樂: + (void)setupMusic:(WXMusicItem *)music;
獲取上一首音樂: + (WXMusicItem *)previous;
獲取下一首音樂: + (WXMusicItem *)next;
- 實現播放音樂,暫停音樂,停止音樂和播放音效API接口方法
3.WXMusicTool工具類實現代碼
#import "WXMusicTool.h"
#import "WXMusicItem.h"
#import <MJExtension.h>
@implementation WXMusicTool
#pragma mark - 靜態變量
/** 所有音樂 */
static NSArray *_musicItems;
/** 當前播放的音樂 */
static WXMusicItem *_playingMusicItem;
/** 獲取所有音樂模型,設置當前默認音樂 */
+ (void)initialize
{
// 從plist文件中獲取所有音樂模型
_musicItems = [WXMusicItem mj_objectArrayWithFilename:@"Musics.plist"];
// 設置當前默認音樂
_playingMusicItem = _musicItems[4];
}
#pragma mark - 播放音樂操作
/** 獲取所有的音樂 */
+ (NSArray *)musics
{
return _musicItems;
}
/** 獲取正在播放的音樂(默認) */
+ (WXMusicItem *)playingMusic
{
return _playingMusicItem;
}
/** 設置播放的音樂 */
+ (void)setupMusic:(WXMusicItem *)music
{
_playingMusicItem = music;
}
/** 獲取上一首音樂 */
+ (WXMusicItem *)previous
{
// 1.獲取當前音樂的下標值
NSInteger currentIndex = [_musicItems indexOfObject:_playingMusicItem];
// 2.獲取上一首音樂的下標值,判斷是否越界
NSInteger previousIndex = currentIndex - 1;
if (previousIndex < 0) {
previousIndex = _musicItems.count - 1;
}
// 3.獲取上一首的音樂
WXMusicItem *previousMusicItem = _musicItems[previousIndex];
// 4.返回上一首音樂
return previousMusicItem;
}
/** 獲取下一首音樂 */
+ (WXMusicItem *)next
{
// 1.獲取當前音樂的下標值
NSInteger currentIndex = [_musicItems indexOfObject:_playingMusicItem];
// 2.獲取下一首音樂的下標值,判斷是否越界
NSInteger nextIndex = currentIndex + 1;
if (nextIndex >= _musicItems.count) {
nextIndex = 0;
}
// 3.獲取下一首的音樂
WXMusicItem *nextMusicItem = _musicItems[nextIndex];
// 4.返回下一首音樂
return nextMusicItem;
}
@end
在主控制器實現播放音樂
- 播放音樂實現
#pragma mark - 播放音樂
- (void)playingMusic
{
// 1.獲取當前音樂
WXMusicItem *playerMusicItem = [WXMusicTool playingMusic];
// 2.更新子控件信息
self.albumView.image = [UIImage imageNamed:playerMusicItem.icon];
self.iconView.image = [UIImage imageNamed:playerMusicItem.icon];
self.songLabel.text = playerMusicItem.name;
self.singerLabel.text = playerMusicItem.singer;
// 3.開始播放音樂
AVAudioPlayer *currentPlayer = [WXAudioTool playMusicWithMusicName:playerMusicItem.filename];
// 3.0 設置代理,用來監聽音樂播放完畢,實現自動切換到下一首的功能
currentPlayer.delegate = self;
self.currentPlayer = currentPlayer;
// 3.1 設置當前播放時間和音樂總時長
self.currentLabel.text = [NSString stringWithTime:currentPlayer.currentTime];
self.totalLabel.text = [NSString stringWithTime:currentPlayer.duration];
// 3.2 更新當前播放按鈕的狀態
self.playOrPauseBtn.selected = self.currentPlayer.isPlaying;
// 3.3 設置當前播放的音樂的歌詞
self.lrcScrollView.lrcFileName = playerMusicItem.lrcname;
// 3.4 將當前播放的音樂的總時長傳給lrcScrollView,用于做鎖屏界面的總時長
self.lrcScrollView.duration = currentPlayer.duration;
// 4.添加旋轉動畫
[self addIconViewAnimate];
// 5.添加定時器(需先移除定時器在添加,避免當前定時器還在運行,又開啟新定時器)
[self removeProgressTimer];
[self addProgressTimer];
// 6.添加更新歌詞定時器
[self removeLrcTimer];
[self addLrcTimer];
// 7.設置默認當前音樂播放時間,總時長和進度條
[self updateProgressInfo];
}
3.添加播放進度定時器,更新播放進度
1.添加定時器步驟,需先移除當前定時器,再添加定時器.
2.將定時器添加到NSRunLoop,并設置NSRunLoopCommonModes模式.
3.歌手圖標旋轉動畫實現,使用基礎核心動畫CABasicAnimation,設置繞z軸旋轉360°無限循環等動畫屬性配置.
實現更新播放進度功能和歌手圖標旋轉功能
- 播放進度定時器創建/移除方法
- 創建播放進度定時器: - (void)addProgressTimer;
- 移除播放進度定時器: - (void)removeProgressTimer;
實時更新播放進度信息
- 更新進度進度實現
- (void)updateProgressInfo {
// 1.更新當前音樂播放時間和當前音樂總時長
self.currentLabel.text = [NSString stringWithTime:self.currentPlayer.currentTime];
self.totalLabel.text = [NSString stringWithTime:self.currentPlayer.duration];
// 2.更新進度條信息
self.progressSlider.value = self.currentPlayer.currentTime / self.currentPlayer.duration;
}
歌手圖標旋轉動畫
- 歌手圖標旋轉動畫實現
- (void)addIconViewAnimate
{
// 1.創建核心動畫,并設置期相關屬性
CABasicAnimation *anim = [CABasicAnimation animation];
// 1.1 設置繞z軸旋轉360°,無限循環等
anim.keyPath = @"transform.rotation.z";
anim.fromValue = @(0);
anim.toValue = @(M_PI * 2);
anim.repeatCount = CGFLOAT_MAX;
anim.duration = 35;
// SINGLE: 1.2 當進入后臺,再進入前臺時,核心動畫會失效,需設置removedOnCompletion屬性為NO,這樣核心動畫就不會失效
// removedOnCompletion: 設置為NO表示動畫完成的時候不要移除.
anim.removedOnCompletion = NO;
// 2.添加動畫到self.iconView.layer
[self.iconView.layer addAnimation:anim forKey:nil];
}
4.處理滾動條
自定義滾動歌詞的WXLrcScrollView,繼承UIScrollView
- 1.在WXLrcScrollView初始化時,添加滾動歌詞的tableView
- 在
initWithCoder
和initWithFrame
方法中開啟分頁功能,初始化和添加tableView
到WXLrcScrollView - 設置子控件tableView的數據源和代理,實現數據源方法,為tableView提供顯示的測試數據
- 在layoutSubviews中添加tableView的約束,并設置tableView背景顏色,分割線,內邊距等,必須在layoutSubviews中設置,否則會tableView中的歌詞播放不同步,背景為白色等問題.
- 在
/** layoutSubviews中布局子控件tableView */
- (void)layoutSubviews
{
[super layoutSubviews];
// 1.布局tableView
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.mas_top);
make.bottom.equalTo(self.mas_bottom);
make.height.equalTo(self.mas_height);
make.left.equalTo(self.mas_left).offset(self.bounds.size.width);
make.right.equalTo(self.mas_right);
make.width.equalTo(self.mas_width);
}];
// SINGLE: 2.清空tableView背景顏色,取消tableView的分割線,設置tableView的內邊距
self.tableView.backgroundColor = [UIColor clearColor];
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.contentInset = UIEdgeInsetsMake(self.tableView.bounds.size.height * 0.5, 0, self.tableView.bounds.size.height * 0.5, 0);
}
5.播放/暫停,上一首,下一首功能實現,音樂播放完畢自動切換到下一首
在storyboard中設置播放按鈕在Normal/Selected狀態下的按鈕顯示的圖片
播放/暫停,上一首,下一首功能實現
-
播放/暫停功能實現
- 切換播放按鈕的狀態
- 判斷是否正在播放,如果當前正在播放則暫停播放,反之則繼續播放
- 暫停播放: 暫停播放,移除定時器,暫停動畫(分類實現暫停和繼續動畫)
- 繼續播放: 繼續播放,開啟定時器,繼續動畫
-
上一首/下一首功能實現
- 使用WXMusicTool工具類獲取上一首/下一首音樂
- 實現切換音樂的方法
示例代碼
/** 下一首 */
- (IBAction)nextMusic {
// 1.獲取下一首音樂
WXMusicItem *nextMusicItem = [WXMusicTool next];
// 2.播放下一首音樂
[self playMusic:nextMusicItem];
}
/** 上一首 */
- (IBAction)previousMusic {
// 1.獲取上一首音樂
WXMusicItem *previousMusicItem = [WXMusicTool previous];
// 2.播放上一首音樂
[self playMusic:previousMusicItem];
}
/** 切換播放的音樂 */
- (void)playMusic:(WXMusicItem *)music {
// 1.獲取當前音樂,并暫停播放
WXMusicItem *curMusicItem = [WXMusicTool playingMusic];
[WXAudioTool pauseMusicWithMusicName:curMusicItem.filename];
// 2.設置當前播放音樂music
[WXMusicTool setupMusic:music];
// 3.開始播放音樂
[self playingMusic];
}
音樂播放完畢自動切換到下一首
- 主控制器的當前播放器遵守
AVAudioPlayerDelegate
代理協議 - 在播放器創建完成時設置代理,用來監聽音樂播放完畢,實現自動切換到下一首的功能
self.currentPlayer.delegate = self; - 實現代理方法
audioPlayerDidFinishPlaying:successfully
,當前音樂正常播放完成后調用.
#pragma mark - <AVAudioPlayerDelegate>代理協議
// SINGLE: 當前音樂播放完成后調用,在<AVAudioPlayerDelegate>代理協議中
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
// flag == YES 表示音樂播放正常停止,播放完畢自動切換到下一首
if (flag) {
// 音樂播放完畢自動切換下一首
[self nextMusic];
}
}
6.設置進度條UISlider的處理
監聽進度條的事件
- 監聽進度條
TouchDown事件
,當UISlider監聽到TouchDown事件時,移除進度條定時器 - 監聽進度條
TouchUpInside事件
,當UISlider監聽到TouchUpInside事件時,更新當前播放進度,并重新開啟定時器 - 監聽到進度條
ValueChange事
件,拖動UISlider時,使用UISlider的value屬性和當前音樂總時長計算當前播放進度,并更新當前播放時間Label的文字信息.
self.currentLabel.text = [NSString stringWithTime:self.progressSlider.value * self.currentPlayer.duration];
進度條添加Tap敲擊手勢
- 在Main.storyboard給UISlider進度條添加點擊Tap手勢
- 監聽敲擊手勢代碼實現
/** 監聽進度條點擊Tap手勢 */
- (IBAction)sliderTap:(UITapGestureRecognizer *)sender {
// 1.獲取當前點的位置
CGPoint curPoint = [sender locationInView:sender.view];
// 2.獲取當前點與總進度的比例
CGFloat ratio = curPoint.x / self.progressSlider.bounds.size.width;
// 3.更新當前播放器的播放時間
self.currentPlayer.currentTime = ratio * self.currentPlayer.duration;
// 4.更新當前播放時間Label,總時長Label的信息和進度條信息,因為updateProgressInfo方法使用self.currentPlayer.currentTime值,所以必須在第3步執行完才能執行updateProgressInfo方法,如果順序反之,則不行
[self updateProgressInfo];
}
/** 實時更新播放進度信息 */
- (void)updateProgressInfo {
// 1.更新當前音樂播放時間和當前音樂總時長
self.currentLabel.text = [NSString stringWithTime:self.currentPlayer.currentTime];
self.totalLabel.text = [NSString stringWithTime:self.currentPlayer.duration];
// 2.更新進度條信息
self.progressSlider.value = self.currentPlayer.currentTime / self.currentPlayer.duration;
}
7.歌詞的解析
歌詞模型類功能的實現
- 歌詞模型類的屬性:
當前行歌詞的播放時間
,歌詞內容
- 實現當前行歌詞的解析,當前行歌詞數據格式: [00:33.20]只是因為在人群中多看了你一眼
#import "WXLrcLineItem.h"
@implementation WXLrcLineItem
/** 創建WXLrcLineItem的對象方法 */
- (instancetype)initWithLrcLineString:(NSString *)lrcLineString
{
if (self = [super init]) {
// 1.解析歌詞
[self lrcStringToItem:lrcLineString];
}
return self;
}
/** 創建WXLrcLineItem的類方法 */
+ (instancetype)lrcLineItemWithLrcLineString:(NSString *)lrcLineString
{
return [[self alloc] initWithLrcLineString:lrcLineString];
}
/** 解析歌詞 */
- (void)lrcStringToItem:(NSString *)lrcLineString
{
// 歌詞數據: [00:33.20]只是因為在人群中多看了你一眼
// 1.以"]"切割歌詞與時間
NSArray *lrcArray = [lrcLineString componentsSeparatedByString:@"]"];
// 2.解析出歌詞內容 只是因為在人群中多看了你一眼
self.name = lrcArray[1];
// 3.解析時間 [00:31.25
NSString *timeString = lrcArray[0];
self.time = [self timeWithTimeString:[timeString substringFromIndex:1]];
}
/** 解析時間 時間數據: 00:31.25 */
- (NSTimeInterval)timeWithTimeString:(NSString *)timeString {
NSInteger min = [[timeString componentsSeparatedByString:@":"][0] integerValue];
NSInteger sec = [[timeString substringWithRange:NSMakeRange(3, 2)] integerValue];
NSInteger mSec = [[timeString componentsSeparatedByString:@"."][1] integerValue];
return min * 60 + sec + mSec * 0.01;
}
@end
創建歌詞文件處理WXLrcTool工具類
- 實現將歌詞文件轉換成歌詞模型數組
/** 傳入本地歌詞文件名,解析歌詞文件 */
+ (NSArray *)lrcToolWithLrcFileName:(NSString *)lrcFileName
{
// 1.獲取歌詞的路徑
NSString *filePath = [[NSBundle mainBundle] pathForResource:lrcFileName ofType:nil];
// 2.讀取歌詞文件數據
NSString *lrcString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
// SINGLE: 3.通過\n字符切割到數組
NSArray *lrcArray = [lrcString componentsSeparatedByString:@"\n"];
/**
歌詞文件頭部信息
[ti:]
[ar:]
[al:]
*/
// 4.遍歷數組,將數組轉模型
NSMutableArray *lrcArrayM = [NSMutableArray array];
for (NSString *lrcLineString in lrcArray) {
// 1.過濾歌詞文件頭部無用信息,不是以[開頭的也過濾掉
if ([lrcLineString hasPrefix:@"[ti:"] || [lrcLineString hasPrefix:@"[ar:"] || [lrcLineString hasPrefix:@"[al:"] || ![lrcLineString hasPrefix:@"["]) {
continue;
}
// 2.解析歌詞數據到模型
WXLrcLineItem *lrcItem = [WXLrcLineItem lrcLineItemWithLrcLineString:lrcLineString];
// 3.添加到可變數組
[lrcArrayM addObject:lrcItem];
}
return lrcArrayM;
}
8.實現歌詞ScrollView歌詞的滾動功能
主界面控制器創建更新歌詞的定時器CADisplayLink定時器
- 提供創建/移除用于更新歌詞的定時器的方法
- 在定時器定時執行的方法中將當前的播放時間傳遞給lrcScrollView的currentTime屬性,讓lrcScrollView實現歌詞的進度更新
self.lrcScrollView.currentTime = self.currentPlayer.currentTime;
自定義顯示歌詞的WXLrcLabel
- 對外提供當前行歌詞的進度progress屬性,讓外部為其設置當前歌詞的進度
- 重寫- (void)drawRect:(CGRect)rect方法繪制WXLrcLabel
-
UIRectFill
會填充Label顏色,不是填充文字顏色. UIRectFill(fullRect); -
UIRectFillUsingBlendMode
填充文字函數,kCGBlendModeSourceOut:表示填充文字以外的區域
-
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
// 1.設置填充的顏色
[[UIColor greenColor] set];
// 2.設置要填充的尺寸,根據傳遞過來的歌詞播放進度
CGRect fullRect = CGRectMake(0, 0, self.bounds.size.width * self.progress, self.bounds.size.height);
// 3.開始繪制
// SINGLE: UIRectFill會填充Label顏色,不是填充文字顏色
// UIRectFill(fullRect);
// SINGLE: UIRectFillUsingBlendMode(fullRect, kCGBlendModeSourceIn)填充文字,kCGBlendModeSourceOut:表示填充文字以外的區域
UIRectFillUsingBlendMode(fullRect, kCGBlendModeSourceIn);
}
- 重寫progress當前行歌詞播放的進度,用于刷新重繪Label,在方法內部調用setNeedsDisplay重繪Label
實現歌詞的滾動,當前行歌詞進度顏色填充效果
- 重寫lrcFileName屬性set方法來設置tableView中的歌詞
-
歌詞bug注意
: 必須將指向當前播放的歌詞行數清0,否則在音樂快播放完成時手動切換下一首會導致程序崩潰.奔潰原因: 假設當前音樂歌詞總60行,下一首音樂歌詞共38行,當前播放到55行是調到下一首,下一首最大才38行,這樣會導致tableView的數據源數組訪問越界.
-
- (void)setLrcFileName:(NSString *)lrcFileName
{
// CARE: 0.切換音樂前,將當前播放的歌詞清0,否則會出現當前音樂快播完后,手動切換下一首導致程序奔潰
// 奔潰原因: 假設當前音樂歌詞總60行,下一首音樂歌詞共38行,當前播放到55行是調到下一首,下一首最大才38行,這樣會導致tableView的數據源數組訪問越界
// 0.指向當前播放的歌詞行數清0
self.currentIndex = 0;
// 1.保存歌詞名
_lrcFileName = lrcFileName;
// 2.解析歌詞,保存到數組
self.lrcList = [WXLrcTool lrcToolWithLrcFileName:lrcFileName];
// CARE: 初始設置歌詞的第0行
WXLrcLineItem *firstItem = self.lrcList[0];
self.lrcLabel.text = firstItem.name;
// 3.刷新tableView
[self.tableView reloadData];
}
- 實時刷新當前歌詞進度
滾動tableView的方法, scrollPosition: UITableViewScrollPositionTop表示tableView滾動到頂部
*- (void)scrollToRowAtIndexPath:(NSIndexPath )indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;計算當前行歌詞的進度
獲取當前行歌詞進度 當前行歌詞進度 = (當前播放的時間 - 當前行歌詞的開始時間) / (下一行歌詞的開始時間 - 當前行歌詞的開始時間)因該方法每秒執行60次,考慮到內部刷新列表操作和性能問題,判斷如果當前行是正在播放的歌詞,就無需刷新,通過self.currentIndex != i,如果不等于i,才進入刷新列表
設置當前行歌詞后,要刷新當前行和上一行歌詞的cell
/** 重寫當前播放時間set方法,該方法每秒會調用60次,因為外部用CADisplayLink定時器刷新歌詞進度 */
- (void)setCurrentTime:(NSTimeInterval)currentTime
{
// 1.保存當前播放時間
_currentTime = currentTime;
// 2.獲取歌詞的總數
NSInteger count = self.lrcList.count;
// 3.遍歷歌詞數組
for (NSInteger i = 0; i < count; i++) {
// 3.1 獲取第i位置的歌詞模型
WXLrcLineItem *currentLrcItem = self.lrcList[i];
// 3.2 獲取第i+1位置的歌詞的模型
NSInteger nextIndex = i + 1;
WXLrcLineItem *nextLrcItem = nil;
if (nextIndex < count) {
nextLrcItem = self.lrcList[nextIndex];
}
// 3.3 判斷當前播放時間是否在第 i ~ i+1歌詞之間 (i位置的時間 <= self.currentTime < i+1位置的時間)
// CARE: 因該方法每秒執行60次,考慮到內部刷新列表操作和性能問題,判斷如果當前行是正在播放的歌詞,就無需刷新,通過self.currentIndex != i,如果不等于i,才進入刷新列表
if ( (self.currentIndex != i) && (currentTime >= currentLrcItem.time && currentTime < nextLrcItem.time) ) {
// 1.滾動當前正在播放的歌詞到中心位置,實際是滾動到最頂部,因為之前有設置內邊距頂部間距是ScrollView的一半
// SINGLE: 調用哪個tableView的滾動方法
NSIndexPath *currentIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView scrollToRowAtIndexPath:currentIndexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
// CARE: 刷新主界面歌詞Label內容
self.lrcLabel.text = currentLrcItem.name;
// 2.刷新上一行歌詞,如果沒刷新,會導致上一行的歌詞字體樣式和當前歌詞的字體樣式一樣
NSIndexPath *previousIndexPath = [NSIndexPath indexPathForRow:self.currentIndex inSection:0];
// CARE: 3.記錄當前滾動的歌詞,下面刷新cell有用到self.currentIndex,此處順序不能和以下相反
self.currentIndex = i;
// 4.刷新當前行和上一行歌詞
[self.tableView reloadRowsAtIndexPaths:@[currentIndexPath, previousIndexPath] withRowAnimation:UITableViewRowAnimationNone];
// 5.設置重新繪制鎖屏封面和歌詞,鎖屏界面
[self setupLockImage];
}
// 4.獲取當前這句歌詞,來獲得當前播放的進度,傳遞當前歌詞進度給cell中lrcLabel
if (self.currentIndex == i) {
// SINGLE: 1.獲取當前行歌詞進度 當前行歌詞進度 = (當前播放的時間 - 當前行歌詞的開始時間) / (下一行歌詞的開始時間 - 當前行歌詞的開始時間)
CGFloat progress = (currentTime - currentLrcItem.time) / (nextLrcItem.time - currentLrcItem.time);
// 2.獲取當前顯示歌詞的cell
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
WXLrcCell *lrcCell = [self.tableView cellForRowAtIndexPath:indexPath];
// 3.設置歌詞進度,傳遞給當前歌詞進度給cell中lrcLabel
lrcCell.lrcLabel.progress = progress;
// CARE: 將當前行歌詞進度賦值給主界面傳過來的歌詞Label;
self.lrcLabel.progress = progress;
}
}
}
10.設置主界面的歌詞
在主界面中設置主界面歌詞
- 在初始化歌詞的ScrollView的setupLrcScrollView方法中初始設置lrcLabel主界面歌詞Label
self.lrcLabel.text = nil;
// 3.將主界面的歌詞的Label傳給lrcScrollView的一個屬性 - >lrcLabel,讓lrcScrollView為其文字屬性,歌詞進度賦值
self.lrcScrollView.lrcLabel = self.lrcLabel;
- 在scrollViewDidScroll方法中,設置self.lrcLabel的透明度,實現拖動時,主界面歌詞漸變效果
/** 監聽歌詞的lrcScrollView的拖動 */
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// 1.獲取當前點
CGPoint curPoint = scrollView.contentOffset;
// 2.通過偏移量獲取滑動的比例
CGFloat ratio = (1 - curPoint.x / self.lrcScrollView.bounds.size.width);
// 3.改變主界面中間歌手圖片iconView和主界面單行歌詞的透明度
self.iconView.alpha = ratio;
self.lrcLabel.alpha = ratio;
}
由lrcScrollView內部為主界面歌詞內容和進度進行更新
- 在WXLrcScrollView的setCurrentTime方法中設置主界面歌詞的內容和進度
- 刷新主界面歌詞Label內容
self.lrcLabel.text = currentLrcItem.name; - 將當前行歌詞進度賦值給主界面傳過來的歌詞Label;
self.lrcLabel.progress = progress;
- 刷新主界面歌詞Label內容
11.實現鎖屏界面信息展示和操作
鎖屏界面項目配置
-
設置項目可播放音視頻步驟
-
配置后臺可播放音視頻 工程文件->Capabilities -> Background modes ->Audio
7.鎖屏界面項目配置.png 創建后臺播放音視頻的會話,并激活會話
-
// 1.創建會話
AVAudioSession *session = [AVAudioSession sharedInstance];
// 2.設置類別為后臺播放 AVAudioSessionCategoryPlayback: 類別為后臺播放,該常量字符串在AVAudioSession.h中
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
// 3.激活會話
[session setActive:YES error:nil];
- 在AppDelegate.m的didFinishLaunchingWithOptions方法中**創建后臺播放音視頻的會話,并激活會話**
```objectivec
// REMARKS: 項目配置后臺可播放音視頻
// SINGLE: 配置后臺可播放音視頻 工程文件->Capabilities -> Background modes ->Audio
// CARE: 模擬器上運行時,音樂可后臺運行,但是真機運行默認是不能后臺播放音視頻的,必須在項目中配置以上操作(后臺可播放音視頻),需創建會話,設置會話類別為后臺播放,并激活會話.
// 為確保程序運行時會執行到以下設置音視頻會話(后臺播放會話)代碼,所以放在didFinishLaunchingWithOptions方法中執行
// 1.創建會話
AVAudioSession *session = [AVAudioSession sharedInstance];
// 2.設置類別為后臺播放 AVAudioSessionCategoryPlayback: 類別為后臺播放,該常量字符串在AVAudioSession.h中
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
// 3.激活會話
[session setActive:YES error:nil];
return YES;
- 設置鎖屏界面顯示的內容步驟
- 獲取鎖屏中心
MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter]; - 設置鎖屏中心要展示的信息,通過設置鎖屏中心nowPlayingInfo屬性設置,該屬性是字典
- 將設置的字典信息賦給nowPlayingInfo屬性
playingInfoCenter.nowPlayingInfo = playingInfoDict; - 讓應用程序開啟遠程事件
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
- 獲取鎖屏中心
// REMARKS: 設置鎖屏界面
/** 設置鎖屏界面 */
- (void)setupLockScreenInfoWithLockImage:(UIImage *)lockImage
{
/*
// 媒體常量
MPMediaItemPropertyAlbumTitle // 媒體音樂的標題(或名稱)
MPMediaItemPropertyAlbumTrackCount
MPMediaItemPropertyAlbumTrackNumber
MPMediaItemPropertyArtist // 作者
MPMediaItemPropertyArtwork // 封面
MPMediaItemPropertyComposer // 音樂劇作曲家的媒體項目
MPMediaItemPropertyDiscCount // 光盤在包含媒體項目的專輯的數目
MPMediaItemPropertyDiscNumber
MPMediaItemPropertyGenre
MPMediaItemPropertyPersistentID
MPMediaItemPropertyPlaybackDuration // 媒體項目的播放持續時間(當前播放時間)
MPMediaItemPropertyTitle // 顯示在作者和標題上面
*/
// REMARKS: 設置鎖屏界面,MPNowPlayingInfoCenter鎖屏中心類在MediaPlayer框架中,所以需導入MediaPlayer/MediaPlayer.h頭文件
// 1.獲取當前正在播放的音樂
WXMusicItem *playingMusicItem = [WXMusicTool playingMusic];
// 2.獲取鎖屏中心
MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
// 3.設置鎖屏中心要展示的信息,通過設置鎖屏中心nowPlayingInfo屬性設置,該屬性是字典
// 創建要可變字典,用來存放要顯示在鎖屏中心的信息
NSMutableDictionary *playingInfoDict = [NSMutableDictionary dictionary];
// 3.1 設置展示的音樂名稱
[playingInfoDict setObject:playingMusicItem.name forKey:MPMediaItemPropertyAlbumTitle];
// 3.2 設置展示的歌手名
[playingInfoDict setObject:playingMusicItem.singer forKey:MPMediaItemPropertyArtist];
// 3.3 設置展示封面
MPMediaItemArtwork *artWork = [[MPMediaItemArtwork alloc] initWithImage:lockImage];
[playingInfoDict setObject:artWork forKey:MPMediaItemPropertyArtwork];
// 3.4 設置音樂播放的總時間
[playingInfoDict setObject:@(self.duration) forKey:MPMediaItemPropertyPlaybackDuration];
// 3.5 設置音樂當前播放的時間
[playingInfoDict setObject:@(self.currentTime) forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
// 3.6 將設置的字典信息賦給nowPlayingInfo屬性
playingInfoCenter.nowPlayingInfo = playingInfoDict;
// SINGLE: 4.讓應用程序開啟遠程事件
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}
12.實現鎖屏鎖屏歌詞展示
繪制鎖屏封面和歌詞
- 將鎖屏的封面圖片和歌詞重新繪制,生成新圖片,在顯示到顯示歌手圖標的UIImageView上
#pragma mark - 設置鎖屏界面和鎖屏歌詞
/** 繪制鎖屏封面和歌詞 */
- (void)setupLockImage
{
// 1.獲取當前音樂的模型
WXMusicItem *currentMusicItem = [WXMusicTool playingMusic];
// 2.從當前音樂模型取出封面圖片
UIImage *currentImage = [UIImage imageNamed:currentMusicItem.icon];
// 3.獲取當前,上一行,下一行歌詞
// 3.1 獲取當前行歌詞
WXLrcLineItem *currentLrcLine = self.lrcList[self.currentIndex];
// 3.2 獲取上一行歌詞
NSInteger previousIndex = self.currentIndex - 1;
WXLrcLineItem *previousLrcLine = nil;
if (previousIndex >= 0) {
previousLrcLine = self.lrcList[previousIndex];
}
// 3.3 獲取下一行歌詞
NSInteger nextIndex = self.currentIndex + 1;
WXLrcLineItem *nextLrcLine = nil;
if (nextIndex < self.lrcList.count) {
nextLrcLine = self.lrcList[nextIndex];
}
// 4.繪制圖片
// 4.1 開啟和圖片尺寸一樣的上下文
UIGraphicsBeginImageContext(currentImage.size);
// 4.2 繪制圖片
[currentImage drawInRect:CGRectMake(0, 0, currentImage.size.width, currentImage.size.height)];
// 4.3 將歌詞文字繪制上去
// 設置文字高度
CGFloat titleH = 32;
// 4.3.1 繪制上一句歌詞和下一句歌詞
// SINGLE: 設置繪制文字居中
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = NSTextAlignmentCenter;
NSDictionary *otherAttr = @{
NSFontAttributeName : [UIFont systemFontOfSize:18],
NSForegroundColorAttributeName : [UIColor yellowColor],
NSParagraphStyleAttributeName : paragraphStyle
};
// 繪制上一句和下一句歌詞(繪制到圖片底部)
[previousLrcLine.name drawInRect:CGRectMake(0, currentImage.size.height - titleH * 3, currentImage.size.width, titleH) withAttributes:otherAttr];
[nextLrcLine.name drawInRect:CGRectMake(0, currentImage.size.height - titleH, currentImage.size.width, titleH) withAttributes:otherAttr];
// 4.3.2 繪制當前行歌詞文字
NSDictionary *currentAttr = @{
NSFontAttributeName : [UIFont systemFontOfSize:24],
NSForegroundColorAttributeName : [UIColor greenColor],
NSParagraphStyleAttributeName : paragraphStyle
};
[currentLrcLine.name drawInRect:CGRectMake(0, currentImage.size.height - titleH * 2, currentImage.size.width, titleH) withAttributes:currentAttr];
// 4.4 生成繪制好的圖片
UIImage *lockImage = UIGraphicsGetImageFromCurrentImageContext();
// 4.5 關閉圖形上下文
UIGraphicsEndImageContext();
// 5.將生成的圖片添加到鎖屏的封面圖片上
[self setupLockScreenInfoWithLockImage:lockImage];
}
鎖屏界面實現播放,暫停,上一首,下一首功能
- 監聽遠程事件,實現鎖屏界面可操作播放/暫停,上一首,下一首
#pragma mark - 監聽遠程事件,實現鎖屏界面可操作播放/暫停,上一首,下一首
// REMARKS: 監聽遠程事件,監聽鎖屏界面操作
/** 監聽遠程事件 */
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{
switch (event.subtype) {
case UIEventSubtypeRemoteControlPlay:
case UIEventSubtypeRemoteControlPause:
[self playOrPause];
break;
case UIEventSubtypeRemoteControlPreviousTrack:
[self previousMusic];
break;
case UIEventSubtypeRemoteControlNextTrack:
[self nextMusic];
break;
default:
break;
}
}