iOS_33_音樂播放(後台播放+鎖屏歌詞)
最終:
應用程式代理程式(後台播放三步曲)
//// BeyondAppDelegate.h// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.//#import @interface BeyondAppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window;@end
//// BeyondAppDelegate.m// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.//#import "BeyondAppDelegate.h"@implementation BeyondAppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ // Override point for customization after application launch. return YES;}- (void)applicationDidEnterBackground:(UIApplication *)application{ // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. // 後台播放三步驟之一:讓應用在後台運行 [application beginBackgroundTaskWithExpirationHandler:nil];}@end
控制器
//// SongController.h// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.// 音樂播放控制器,繼承自tableViewCtrl#import @interface SongController : UITableViewController@end
//// SongController.m// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.// 音樂播放控制器,繼承自tableViewCtrl#import "SongController.h"// 核心架構,必須匯入,鎖屏顯歌詞#import #import // 模型#import "Song.h"// 視圖#import "SongCell.h"// 工具#import "SongTool.h"#pragma mark - 類擴充@interface SongController () // 對象數組@property (strong, nonatomic) NSArray *songArr;// 當前選中的那一行所對應的播放器(暫沒用到)@property (nonatomic, strong) AVAudioPlayer *currentPlayingAudioPlayer;// 播放器的代理方法中,沒有監聽歌曲播放進度的方法// 只能自己 開一個定時器,即時監聽歌曲的播放進度@property (nonatomic, strong) CADisplayLink *link;- (IBAction)jump:(id)sender;@end@implementation SongController- (void)viewDidLoad{ [super viewDidLoad]; // 防止被導覽列蓋住 self.tableView.contentInset = UIEdgeInsetsMake(20, 0, 0, 0);}#pragma mark - 懶載入- (NSArray *)songArr{ if (!_songArr) { // 分類方法,一步轉對象數組 self.songArr = [Song objectArrayWithFilename:@"SongArr.plist"]; } return _songArr;}// 播放器的代理方法中,沒有監聽歌曲播放進度的方法// 只能自己 開一個定時器,即時監聽歌曲的播放進度- (CADisplayLink *)link{ if (!_link) { self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; } return _link;}#pragma mark - 時鐘方法// 播放器的代理方法中,沒有監聽歌曲播放進度的方法// 只能自己 開一個定時器,即時監聽歌曲的播放進度// 即時更新(1秒中調用60次)- (void)update{ // NSLog(@"總長:%f 當前播放:%f", self.currentPlayingAudioPlayer.duration, self.currentPlayingAudioPlayer.currentTime);#warning 調整鎖屏時的歌詞}#pragma mark - 連線方法- (IBAction)jump:(id)sender{ // 控制播放進度(單位:秒) self.currentPlayingAudioPlayer.currentTime = self.currentPlayingAudioPlayer.duration - 3;}#pragma mark - AVAudioPlayer代理方法// 一曲播放完畢時調用,停止動畫,並跳至下一首播放- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{ // 1.先得當前播放完畢的這首歌的行號,通過它,計算下一行的行號(防越界) NSIndexPath *selectedPath = [self.tableView indexPathForSelectedRow]; int nextRow = selectedPath.row + 1; if (nextRow == self.songArr.count) { nextRow = 0; } // 2.停止上一首歌的轉圈 (修改模型,並再次傳遞模型給自訂cell) SongCell *cell = (SongCell *)[self.tableView cellForRowAtIndexPath:selectedPath]; Song *music = self.songArr[selectedPath.row]; music.playing = NO; // 內部會攔截setter方法,並停止轉圈動畫 cell.music = music; // 3.播放下一首歌 (選中並滾動至對應位置) NSIndexPath *nextPath = [NSIndexPath indexPathForRow:nextRow inSection:selectedPath.section]; // 介面上選中 [self.tableView selectRowAtIndexPath:nextPath animated:YES scrollPosition:UITableViewScrollPositionTop]; // 手動調用代理方法 [self tableView:self.tableView didSelectRowAtIndexPath:nextPath];}#pragma mark 播放被打斷和恢複// 音樂播放器被打斷 (如開始 打、接電話)- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player{ // 會自動暫停 do nothing ... NSLog(@"audioPlayerBeginInterruption---被打斷");}// 音樂播放器打斷終止 (如結束 打、接電話)- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags{ // 手動恢複播放 [player play]; NSLog(@"audioPlayerEndInterruption---打斷終止");}#pragma mark - 資料來源// 多少行- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return self.songArr.count;}// 每一行獨一無二的的cell (給自訂cell傳遞模型)- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ // 1.自訂cell的類方法,快速建立cell SongCell *cell = [SongCell cellWithTableView:tableView]; // 2.設定cell的資料來源 cell.music = self.songArr[indexPath.row]; // 3.返回產生並填充好的cell return cell;}// 每一行的高度- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 70;}#pragma mark - 代理方法// 選中一行,播放對應的音樂- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ // 1.取得該行對應的模型,並修改其isPlaying屬性 Song *music = self.songArr[indexPath.row]; if (music.isPlaying) { return; } // 屬性【現正播放】賦值為真 music.playing = YES; // 2.傳遞資料來源模型 給工具類播放音樂 AVAudioPlayer *audioPlayer = [SongTool playMusic:music.filename]; audioPlayer.delegate = self; self.currentPlayingAudioPlayer = audioPlayer; // 3.重要~~~在鎖定畫面顯示歌曲資訊 [self showInfoInLockedScreen:music]; // 4.開啟定時器,監聽播放進度 (先關閉舊的) [self.link invalidate]; self.link = nil; [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; // 5.再次傳遞資料來源模型 給自訂cell(執行轉圈動畫) SongCell *cell = (SongCell *)[tableView cellForRowAtIndexPath:indexPath]; cell.music = music;}// 取消選中一行時,停止音樂,動畫- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath{ // 1.取得該行對應的模型,並修改其isPlaying屬性 Song *music = self.songArr[indexPath.row]; music.playing = NO; // 2.根據音樂名,停止音樂(內部會遍曆) [SongTool stopMusic:music.filename]; // 3.再次傳遞資料來源模型 給自訂cell(停止轉圈) SongCell *cell = (SongCell *)[tableView cellForRowAtIndexPath:indexPath]; cell.music = music;}#pragma mark - 鎖屏顯歌詞// 在鎖定畫面顯示歌曲資訊(即時換圖片MPMediaItemArtwork可以達到即時換歌詞的目的)- (void)showInfoInLockedScreen:(Song *)music{ // 健壯性寫法:如果存在這個類,才能在鎖屏時,顯示歌詞 if (NSClassFromString(@"MPNowPlayingInfoCenter")) { // 核心:字典 NSMutableDictionary *info = [NSMutableDictionary dictionary]; // 標題(音樂名稱) info[MPMediaItemPropertyTitle] = music.name; // 藝術家 info[MPMediaItemPropertyArtist] = music.singer; // 專輯名稱 info[MPMediaItemPropertyAlbumTitle] = music.singer; // 圖片 info[MPMediaItemPropertyArtwork] = [[MPMediaItemArtwork alloc] initWithImage:[UIImage imageNamed:music.icon]]; // 唯一的API,單例,nowPlayingInfo字典 [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = info; }}@end
模型Model
//// Song.h// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.// 模型,一首歌曲,成員很多#import @interface Song : NSObject// 歌名@property (copy, nonatomic) NSString *name;// 檔案名稱,如@"a.mp3"@property (copy, nonatomic) NSString *filename;// 藝術家@property (copy, nonatomic) NSString *singer;// 藝術家頭像@property (copy, nonatomic) NSString *singerIcon;// 藝術家大圖片(鎖屏的時候用)@property (copy, nonatomic) NSString *icon;// 標記,用於轉動頭像@property (nonatomic, assign, getter = isPlaying) BOOL playing;@end
自訂View SongCell
//// SongCell.h// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.// View,自訂cell,依賴模型#import // View 需依賴模型@class Song;@interface SongCell : UITableViewCell// 資料來源模型@property (nonatomic, strong) Song *music;// 控制器知道得最少+ (instancetype)cellWithTableView:(UITableView *)tableView;@end
//// SongCell.m// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.// View,自訂cell,依賴模型#import "SongCell.h"// 模型,資料來源#import "Song.h"#import "Colours.h"#pragma mark - 類擴充@interface SongCell ()// 用於 頭像的旋轉 CGAffineTransformRotate,一秒鐘轉45度@property (nonatomic, strong) CADisplayLink *link;@end@implementation SongCell#pragma mark - 懶載入- (CADisplayLink *)link{ if (!_link) { self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; } return _link;}#pragma mark - 供外界調用+ (instancetype)cellWithTableView:(UITableView *)tableView{ static NSString *ID = @"music"; SongCell *cell = [tableView dequeueReusableCellWithIdentifier:ID]; if (cell == nil) { cell = [[SongCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID]; } return cell;}// 攔截setter方法,為內部子控制項賦值,並進行轉圈動畫- (void)setMusic:(Song *)music{ _music = music; // 設定獨一無二的資料 self.textLabel.text = music.name; self.detailTextLabel.text = music.singer; // 分類方法,建立一個圓形的邊框 self.imageView.image = [UIImage circleImageWithName:music.singerIcon borderWidth:2 borderColor:[UIColor skyBlueColor]]; // 如果模型的屬性isPlaying為真,則開始CGAffineTransformRotate if (music.isPlaying) { [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; } else { // 如果模型的isPlaying為假,則停止時鐘動畫,並將CGAffineTransformRotate歸零 [self.link invalidate]; self.link = nil; self.imageView.transform = CGAffineTransformIdentity; }}#pragma mark - 時鐘方法// 角速度 : 45°/s ,即8秒轉一圈- (void)update{ // deltaAngle = 1/60秒 * 45 // 兩次調用之間 轉動的角度 == 時間 * 速度 CGFloat angle = self.link.duration * M_PI_4; // 不用核心動畫,是因為 進入後台之後,動畫就停止了 self.imageView.transform = CGAffineTransformRotate(self.imageView.transform, angle);}@end
音樂播放工具類
//// SongTool.h// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.//#import // 音樂工具類,必須匯入AVFoundation的主標頭檔#import @interface SongTool : NSObject// 類方法, 播放音樂, 參數:音樂檔案名稱 如@"a.mp3",同時為了能夠給播放器AVAudioPlayer對象設定代理,在建立好播放器對象後,將其返回給調用者// 設定代理後,可以監聽播放器被打斷和恢複打斷+ (AVAudioPlayer *)playMusic:(NSString *)filename;// 類方法, 暫停音樂, 參數:音樂檔案名稱 如@"a.mp3"+ (void)pauseMusic:(NSString *)filename;// 類方法, 停止音樂, 參數:音樂檔案名稱 如@"a.mp3",記得從字典中移除+ (void)stopMusic:(NSString *)filename;// 返回當前現正播放的音樂播放器,方便外界控制其快進,後退或其他方法+ (AVAudioPlayer *)currentPlayingAudioPlayer;@end
//// SongTool.m// 33_音效//// Created by beyond on 14-9-10.// Copyright (c) 2014年 com.beyond. All rights reserved.//#import "SongTool.h"@implementation SongTool// 字典,存放所有的音樂播放器,鍵是:音樂名,值是對應的音樂播放器對象audioPlayer// 一首歌對應一個音樂播放器static NSMutableDictionary *_audioPlayerDict;#pragma mark - Life Cycle+ (void)initialize{ // 字典,鍵是:音樂名,值是對應的音樂播放器對象 _audioPlayerDict = [NSMutableDictionary dictionary]; // 設定後台播放 [self sutupForBackgroundPlay];}// 設定後台播放+ (void)sutupForBackgroundPlay{ // 後台播放三步曲之第三步,設定 音頻會話類型 AVAudioSession *session = [AVAudioSession sharedInstance]; // 類型是:播放和錄音 [session setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; // 而且要啟用 音頻會話 [session setActive:YES error:nil];}#pragma mark - 供外界調用// 類方法, 播放音樂, 參數:音樂檔案名稱 如@"a.mp3"// 同時為了能夠給播放器AVAudioPlayer對象設定代理,在建立好播放器對象後,將其返回給調用者// 設定代理後,可以監聽播放器被打斷和恢複打斷+ (AVAudioPlayer *)playMusic:(NSString *)filename{ // 健壯性判斷 if (!filename) return nil; // 1.先從字典中,根據音樂檔案名稱,取出對應的audioPlayer AVAudioPlayer *audioPlayer = _audioPlayerDict[filename]; if (!audioPlayer) { // 如果沒有,才需建立對應的音樂播放器,並且存入字典 // 1.1載入音樂檔案 NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:nil]; // 健壯性判斷 if (!url) return nil; // 1.2根據音樂的URL,建立對應的audioPlayer audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil]; // 1.3開始緩衝 [audioPlayer prepareToPlay]; // 如果要實現變速播放,必須同時設定下面兩個參數 // audioPlayer.enableRate = YES; // audioPlayer.rate = 10.0; // 1.4最後,放入字典 _audioPlayerDict[filename] = audioPlayer; } // 2.如果是暫停或停止時,才需要開始播放 if (!audioPlayer.isPlaying) { [audioPlayer play]; } // 3.返回建立好的播放器,方便調用者設定代理,監聽播放器的進度currentTime return audioPlayer;}// 類方法, 暫停音樂, 參數:音樂檔案名稱 如@"a.mp3"+ (void)pauseMusic:(NSString *)filename{ // 健壯性判斷 if (!filename) return; // 1.先從字典中,根據key【檔案名稱】,取出audioPlayer【肯定 有 值】 AVAudioPlayer *audioPlayer = _audioPlayerDict[filename]; // 2.如果是現正播放,才需要暫停 if (audioPlayer.isPlaying) { [audioPlayer pause]; }}// 類方法, 停止音樂, 參數:音樂檔案名稱 如@"a.mp3",記得從字典中移除+ (void)stopMusic:(NSString *)filename{ // 健壯性判斷 if (!filename) return; // 1.先從字典中,根據key【檔案名稱】,取出audioPlayer【肯定 有 值】 AVAudioPlayer *audioPlayer = _audioPlayerDict[filename]; // 2.如果是現正播放,才需要停止 if (audioPlayer.isPlaying) { // 2.1停止 [audioPlayer stop]; // 2.2最後,記得從字典中移除 [_audioPlayerDict removeObjectForKey:filename]; }}// 返回當前現正播放的音樂播放器,方便外界控制其快進,後退或其他方法+ (AVAudioPlayer *)currentPlayingAudioPlayer{ // 遍曆字典的鍵,再根據鍵取出值,如果它是現正播放,則返回該播放器 for (NSString *filename in _audioPlayerDict) { AVAudioPlayer *audioPlayer = _audioPlayerDict[filename]; if (audioPlayer.isPlaying) { return audioPlayer; } } return nil;}@end
圖片加圓圈邊框的分類
//// UIImage+Circle.h// 33_音效//// Created by beyond on 14-9-15.// Copyright (c) 2014年 com.beyond. All rights reserved.// 圓形邊框#import @interface UIImage (Circle)+ (instancetype)circleImageWithName:(NSString *)name borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor;@end
//// UIImage+Circle.m// 33_音效//// Created by beyond on 14-9-15.// Copyright (c) 2014年 com.beyond. All rights reserved.//#import "UIImage+Circle.h"@implementation UIImage (Circle)+ (instancetype)circleImageWithName:(NSString *)name borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor{ // 1.載入原圖 UIImage *oldImage = [UIImage imageNamed:name]; // 2.開啟上下文 CGFloat imageW = oldImage.size.width + 2 * borderWidth; CGFloat imageH = oldImage.size.height + 2 * borderWidth; CGSize imageSize = CGSizeMake(imageW, imageH); UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0); // 3.取得當前的上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 4.畫邊框(大圓) [borderColor set]; CGFloat bigRadius = imageW * 0.5; // 大圓半徑 CGFloat centerX = bigRadius; // 圓心 CGFloat centerY = bigRadius; CGContextAddArc(ctx, centerX, centerY, bigRadius, 0, M_PI * 2, 0); CGContextFillPath(ctx); // 畫圓 // 5.小圓 CGFloat smallRadius = bigRadius - borderWidth; CGContextAddArc(ctx, centerX, centerY, smallRadius, 0, M_PI * 2, 0); // 裁剪(後面畫的東西才會受裁剪的影響) CGContextClip(ctx); // 6.畫圖 [oldImage drawInRect:CGRectMake(borderWidth, borderWidth, oldImage.size.width, oldImage.size.height)]; // 7.取圖 UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); // 8.結束上下文 UIGraphicsEndImageContext(); return newImage;}@end