首页 > 代码库 > iOS_33_音乐播放(后台播放+锁屏歌词)

iOS_33_音乐播放(后台播放+锁屏歌词)

最终效果图:

应用程序代理(后台播放三步曲)
//
//  BeyondAppDelegate.h
//  33_音效
//
//  Created by beyond on 14-9-10.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface BeyondAppDelegate : UIResponder <UIApplicationDelegate>

@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 <UIKit/UIKit.h>

@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 <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

// 模型
#import "Song.h"
// 视图
#import "SongCell.h"
// 工具
#import "SongTool.h"



#pragma mark - 类扩展

@interface SongController () <AVAudioPlayerDelegate>

// 对象数组
@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 <Foundation/Foundation.h>

@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 <UIKit/UIKit.h>

// 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 <Foundation/Foundation.h>
// 音乐工具类,必须导入AVFoundation的主头文件
#import <AVFoundation/AVFoundation.h>

@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 <UIKit/UIKit.h>

@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






iOS_33_音乐播放(后台播放+锁屏歌词)