你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS_33_音樂播放(後台播放+鎖屏歌詞)

iOS_33_音樂播放(後台播放+鎖屏歌詞)

編輯:IOS開發綜合
最終效果圖: \
應用程序代理(後台播放三步曲)
//
//  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






  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved