iOS开发--AVPlayer实现音乐播放器
来源:互联网 发布:淘宝厂家直销违规吗 编辑:程序博客网 时间:2024/05/01 23:49
iOS开发--AVPlayer实现音乐播放器
版权声明:本文为博主原创文章,未经博主允许不得转载。
目录(?)[+]
这是一篇教学Blog. 重点不完全在播放器上, 目的是通过这个过程掌握以下知识点:
- 单例
- block传值
- 多线程
- 代理传值
- 通知
- 观察者
- 网络请求
- 数据解析
- 多控件布局
- 开发模式和框架设计
今天敲一个音乐播放器, 音乐源我就不共享了, 涉及到版权保护, 别问我的源是哪儿来的. 不告诉你们
这篇博客是一篇教学Blog, Demo不能直接用作生产, 但其中的逻辑是经得起推敲, UI部分美化美化一下即可. 要做到举一反三.
开始敲之前, 我们先看看当前可供使用的多媒体播放框架有哪些
简单介绍一下:
- AudioToolbox.framework的音频播放时间不能超过30s,数据必须是PCM或者IMA4格式,音频文件必须打包成.caf、.aif、.wav中的一种(注意这是官方文档的说法,实际测试发现一些.mp3也可以播放. 它的主要用途可以用作app的音效(不是背景音).
- MediaPlayer.framework框架下有两个常用的系统封装好的播放器:
MPMoviePlayController 和 MPMoviePlayViewController, 二者的区别在于, 后者的视频图像需要一张View视图作为载体, 你可以自己创建这个View, 那么也可以自由的控制它. 最明显的例子就是你可以用它做个浮窗播放器.- AVFoundation.framework 目前被AVKit框架替代了, 但是我没有跟进, 我就用它:
AVAudioRecorder播放器, 提供录音, 录音的的代码加起来没你jj长.
AVPlayer播放器, 一个能播放网络和本地视频/音频的播放器, 和MediaPlayer.framework框架下的两个播放器不同, 系统并未提供它的UI界面, 我们需要自己实现, 往好听了说: 这是一个可以高度自定义的播放器.
AVAudioPlayer与 AVPlayer播放器的区别在于, 这货只能播放本地音乐.
另附一张表格, 里面登记了大多数播放器的优缺点, 图片来源网络:
下面开始我们的音乐播放器之旅.
一. 产品原型图
当我们在实际生产过程中, 作为App前端开发工程师, 我们会拿到产品模型(原型图), 这个模型可能使用墨刀
为你精准绘制, 也可能某个页面使用草纸
为你勾勒, 不管怎样, 你肯定能拿到下面的东西, 这些图片,描绘了你要做的app大概长成什么样子.
歌曲列表
播放界面
播放界面滑动CD还有歌词呢
我们要做的就是上面样式的播放器, 如果您觉得太low, 请左右上角.
二. 功能模块划分:
括号后面的字母作为标记, 后面实现方法中会有这个字母, 您可以根据标记本节来查看当前代码属于哪一模块.
View层:两个界面
1.歌曲列表: 第一个界面是一个TableView界面.(A)
2.播放界面: 一个自定义界面, 需要我们布局.(B)Controller层:
上述两个View的控制器:
1.歌曲列表TableView的控制器.(C)
2.播放界面的控制器.(D)Model层:两个模型
1.歌曲信息模型, 存放每首歌曲的名称, 时长, url, 缩略图, 封面, 歌词等信息.(E)
2.歌词模型, 歌词的基本格式, 这里是[00:01]我大声说我爱的就是我
字符串格式.(F)
很多时候, 我们将一些功能模块单独独立起来, 做一次封装, 封装的好处, 找个机会开篇blog.
- Tools 工具封装:
1.一个从网络中获取歌曲信息的方法.(G)
2.能将MP3文件播放出声音的类(对AVPlayer的的封装).(H)
我们先按照上面的思路进行, 这个模块划分的原则因人而异, 当前项目比较简单, 无论怎么划分都不会产生太大差异性, 不过如果项目足够大, 一个有经验的开发者和新手之间的差距就体现出来了.
按照上述模块划分, 我们需要8个类, 每个类完成自己独特的功能, 所谓”工欲善其事, 必先利其器”, 我们打算从工具类Tools开始, 深入浅出, 然后深入, 深入, 再深入.
三.工具类Tools
3.1 数据请求(G)
在这个工具类里面, 我们将这个类设置成为一个单例类
将数据请求封装成单例的好处是显而易见,
- 首先做到了Model和Controllerc层的完全剥离, 从C层中调用数据请求的方法, 将请求回来的数据存放在单例类中, 也可以回传给C层做进一步的处理使用.
- 其次, 如果歌曲清单没有改变, 那么一次请求的数据应该贯穿应用程序的整个声明周期. 这样, APP运行的任何时刻, 我们都能获取到这个歌曲信息.
- 最后, 所有的页面(我们的APP只有两个页面)都可能使用某首歌曲的信息, 将数据存放到单例的另一个好处就是, 数据伴随单例, 扩大了作用域范围, 与上一条连用, 我们做到了任何页面任何时刻, 都可以随意的访问数据内容.
新建一个类, 继承NSObject, 名称为: GetDataTools
GetDataTools.h
1234567891011121314151617
#import <Foundation/Foundation.h>// 定义blocktypedef void (^PassValue)(NSArray * array);@interface GetDataTools : NSObject// 作为单例的属性,这个数组可以在任何位置,任何时间被访问.@property(nonatomic,strong)NSMutableArray * dataArray;// 单例方法+(instancetype)shareGetData;// 根据传入的URL,通过Block返回一个数组.-(void)getDataWithURL:(NSString *)URL PassValue:(PassValue)passValue;// 根据传入的Index,返回一个"歌曲信息的模型",这个模型来自上面的属性数组.-(MusicInfoModel *)getModelWithIndex:(NSInteger)index;@end
GetDataTools.m
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
#import "GetDataTools.h"static GetDataTools * gd = nil;@implementation GetDataTools// 单例方法, 这个单例方法是不完全的, 如果C层开发者使用了[alloc init]的方式创建对象, 仍不为单例, 正确的封闭其他所有init方法, 或者重写调用我们当前的方法返回对象.+(instancetype)shareGetData{ if (gd == nil) { static dispatch_once_t once_token; dispatch_once(&once_token, ^{ gd = [[GetDataTools alloc] init]; }); } return gd;}// 传入URL, 通过Block返回歌曲信息列表队列.-(void)getDataWithURL:(NSString *)URL PassValue:(PassValue)passValue{ // 这里为什么要用子线程? // 因为,这里请求数据时:arrayWithContentsOfURL方法是同步请求(请求不结束,主线程什么也干不了) // 所以,为了规避这种现象,我们将请求的动作放到子线程中. // 创建线程队列(全局), 改天写个多线程的blog. dispatch_queue_t globl_t = dispatch_get_global_queue(0, 0); // 定义子线程的内容. dispatch_async(globl_t, ^{ // 在这对花括号内的所有操作都不会阻塞主线程了哦 // 请求数据 NSArray * array =[NSArray arrayWithContentsOfURL:[NSURL URLWithString:URL]]; // 解析,将解析好的"歌曲信息模型", 加入我们的属性数组, 以便外界能随时访问. for (NSDictionary * dict in array) { MusicInfoModel * model = [[MusicInfoModel alloc] init]; [model setValuesForKeysWithDictionary:dict]; [self.dataArray addObject:model]; } // !!!Block回传值 passValue(self.dataArray); });}// 属性数组的懒加载(并不是必须用懒加载, 懒加载有懒加载的好处)-(NSMutableArray *)dataArray{ if (_dataArray == nil) { _dataArray = [NSMutableArray array]; } return _dataArray;}// 根据传入的index返回一个"歌曲信息模型"-(MusicInfoModel *)getModelWithIndex:(NSInteger)index{ return self.dataArray[index];}@end
在这个类中, 我们定义了三个方法:
- shareGetData; 单例方法, 单例的好处不在此处赘述. 单例很重要, 一定要熟练掌握.
- getDataWithURL: PassValue:; 这个方法是本类的核心功能了, 从网络中请求数据(异步), 通过block将数组返回. 注意: 这个类本身有一个成员变量_dataArray, 它里面存放了所有的歌曲信息, 其他页面可以通过单例.dataArray方法获取到, 但是我们这里仍然封装其返回一个新的数组的方法. 不为别的, 就是因为这种方式太重要了 – 为了使用而使用.
- getModelWithIndex; 您可能不明为为什么要写一个这么个方法,这个方法的产生是有后面的逻辑背景的, 这里因为blog书写不便, 就直接写在这里了.
以上是我们的数据请求类(GetDataTools).
3.2 播放器工具类(H)
在封装AVPlayer之前, 我们先了解一下AVPlayer有什么特点.
AVPlayer存在于AVFoundation中, 它更加接近于底层, 所以灵活性也更强,AVPlayer本身并不能显示视频,而且它也不像MPMoviePlayerController有一个view属性。如果AVPlayer要显示必须创建一个播放器层AVPlayerLayer用于展示,播放器层继承于CALayer,有了AVPlayerLayer之添加到控制器视图的layer中即可。要使用AVPlayer首先了解一下几个常用的类和它的属性/方法(本段源自网络):
- AVAsset: 属性, 主要用于获取多媒体信息,是一个抽象类,不能直接使用。
- AVURLAsset: 属性, AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。
- AVPlayerItem: 属性,一个媒体资源管理对象,管理者视音频的一些基本信息和状态,一个AVPlayerItem对应着一个视音频资源.
- replaceCurrentItemWithPlayerItem: 替换AVPlayer的当前Item.
- play: 方法, 播放媒体.
- pause: 方法, 暂停.
- seekToTime:completionHandler: 方法, 播放跳转, 调整播放进度.
下面我们解释一下AVPlayerItem:
AVPlayerItem
我们不妨做一次角色扮演游戏, 我是老板, 你是一位员工小张, 主要负责向客户推销一款产品.
今天早上我对你下达了这样命令:
小张, 隔壁的桌子上有一款新开发的产品, 你现在拿着它去给展厅的客户们介绍一下.
上面的命令透露出两个信息:
1.这个款产品是新产品, 你从来没有听说过, 你不知道它的任何参数.
2.你需要立刻完成这件事.
你不傻掉才怪. 这样的命令式不合逻辑, 不合设计思路的.
对比下面的命令:
小张, 这里有份资料, 里面记录着隔壁桌子上新产品的详细信息, 你拿去研究一下, 等到你完全掌握并且准备好时, 你告诉我, 我给你安排一个展厅向客户介绍它.
这条命令透露出的信息:
1.这个产品有说明书
2.你别着急, 慢慢研究, 研究好了你告诉我, 这个时间我(老板)先干点别的.
很明显下面的方式要好于上面.
同样, 你对AVPlayer下达命令也不能采用第一种方式, 你要告诉它, 你要播放的歌曲是什么名字, 有多长时间, 文件在什么位置, 歌曲的图片是什么, 这些东西你要给它写一份详细的说明书.
这个说明书就是AVPlayerItem.
每一个AVPlayer对象, 都有一个自己的AVPlayerItem属性, 名字叫做:currentItem, 我们可以通过replaceCurrentItemWithPlayerItem:
方法来替换当前的Item, 将准备好的Item, 交给Player.
这个过程我们使用观察者模式模式来监视AVPlayerItem的准备情况. 一旦准备完毕, 会修改自身的status属性为AVPlayerItemStatusReadyToPlay
枚举值, 一旦观察到这种状态, 我们就开始真正的播放.方法: play 和 pause
这个两个是AVPlayer的播放控制方法, 我们在控制界面有个按钮, 点一下就播放, 再点一下就暂停, 反复重复. 貌似没有什么, 但是这里有个棘手的问题, AVPlayer的对象成员变量中, 居然没有来标识当前播放状态的! 也就是说, 你永远也不可能直接的获得当前AVPlayer正在播放中或者暂停了.
通常情况下, 我们通过AVPlayer的一个rate(播放速率)来间接得到播放状态, rate==0则暂停, 不为0则正在播放中.切换歌曲
AVPlayer并没有直接提供下一曲和上一曲的的功能, 但是我们可以通过上面的replaceCurrentItemWithPlayerItem:
方法, 将AVPlayer对象的Item替换掉, 之后让它播放, 就可以达到这个效果.
新建一个类, 继承NSObject, 名称为: MusicPlayTools
MusicPlayTools.h
123456789101112131415161718192021222324252627282930313233343536
#import <Foundation/Foundation.h>#import <AVFoundation/AVFoundation.h>// !!! 与block回传值作比较.// 定义协议. 通过代理方法返回当前歌曲的播放进度.// 如果外界想使用本播放器,必须遵循和实现协议中的两个方法.@protocol MusicPlayToolsDelegate <NSObject>// 外界实现这个方法的同时, 也将参数的值拿走了, 这样我们起到了"通过代理方法向外界传递值"的功能.-(void)getCurTiem:(NSString *)curTime Totle:(NSString *)totleTime Progress:(CGFloat)progress;// 播放结束之后, 如何操作由外部决定.-(void)endOfPlayAction;@end@interface MusicPlayTools : NSObject// 本类中的播放器指针.@property(nonatomic,strong)AVPlayer * player;// 本类中的,播放中的"歌曲信息模型"@property(nonatomic,strong)MusicInfoModel * model;// 代理@property(nonatomic,weak)id<MusicPlayToolsDelegate> delegate;// 单例方法+(instancetype)shareMusicPlay;// 播放音乐-(void)musicPlay;// 暂停音乐-(void)musicPause;// 准备播放-(void)musicPrePlay;// 跳转-(void)seekToTimeWithValue:(CGFloat)value;// 返回一个歌词数组-(NSMutableArray *)getMusicLyricArray;// 根据当前播放时间,返回 对应歌词 在 数组 中的位置.-(NSInteger)getIndexWithCurTime;@end
MusicPlayTools.m
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
#import "MusicPlayTools.h"static MusicPlayTools * mp = nil;@interface MusicPlayTools ()@property(nonatomic,strong)NSTimer * timer;@end@implementation MusicPlayTools// 单例方法+(instancetype)shareMusicPlay{ if (mp == nil) { static dispatch_once_t once_token; dispatch_once(&once_token, ^{ mp = [[MusicPlayTools alloc] init]; }); } return mp;}// 这里为什么要重写init方法呢?// 因为,我们应该得到 "某首歌曲播放结束" 这一事件,之后由外界来决定"播放结束之后采取什么操作".// AVPlayer并没有通过block或者代理向我们返回这一状态(事件),而是向通知中心注册了一条通知(AVPlayerItemDidPlayToEndTimeNotification),我们也只有这一条途径获取播放结束这一事件.// 所以,在我们创建好一个播放器时([[AVPlayer alloc] init]),应该立刻为通知中心添加观察者,来观察这一事件的发生.// 这个动作放到init里,最及时也最合理.- (instancetype)init{ self = [super init]; if (self) { _player = [[AVPlayer alloc] init]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(endOfPlay:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; } return self;}// 播放结束后的方法,由代理具体实现行为.-(void) endOfPlay:(NSNotification *)sender{ // 为什么要先暂停一下呢? // 看看 musicPlay方法, 第一个if判断,你能明白为什么吗? [self musicPause]; [self.delegate endOfPlayAction];}// 准备播放,我们在外部调用播放器播放时,不会调用"直接播放",而是调用这个"准备播放",当它准备好时,会直接播放.-(void)musicPrePlay{ // 通过下面的逻辑,只要AVPlayer有currentItem,那么一定被添加了观察者. // 所以上来直接移除之. if (self.player.currentItem) { [self.player.currentItem removeObserver:self forKeyPath:@"status"]; } // 根据传入的URL(MP3歌曲地址),创建一个item对象 // initWithURL的初始化方法建立异步链接. 什么时候连接建立完成我们不知道.但是它完成连接之后,会修改自身内部的属性status. 所以,我们要观察这个属性,当它的状态变为AVPlayerItemStatusReadyToPlay时,我们便能得知,播放器已经准备好,可以播放了. AVPlayerItem * item = [[ AVPlayerItem alloc] initWithURL:[NSURL URLWithString:self.model.mp3Url]]; // 为item的status添加观察者. [item addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil]; // 用新创建的item,替换AVPlayer之前的item.新的item是带着观察者的哦. [self.player replaceCurrentItemWithPlayerItem:item];}// 观察者的处理方法, 观察的是Item的status状态.-(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ if ([keyPath isEqualToString:@"status"]) { switch ([[change valueForKey:@"new"] integerValue]) { case AVPlayerItemStatusUnknown: NSLog(@"不知道什么错误"); break; case AVPlayerItemStatusReadyToPlay: // 只有观察到status变为这种状态,才会真正的播放. [self musicPlay]; break; case AVPlayerItemStatusFailed: // mini设备不插耳机或者某些耳机会导致准备失败. NSLog(@"准备失败"); break; default: break; } }}// 播放-(void)musicPlay{ // 如果计时器已经存在了,说明已经在播放中,直接返回. // 对于已经存在的计时器,只有musicPause方法才会使之停止和注销. if (self.timer != nil) { return; } // 播放后,我们开启一个计时器. self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(timerAction:) userInfo:nil repeats:YES]; [self.player play];}-(void)timerAction:(NSTimer * )sender{ // !! 计时器的处理方法中,不断的调用代理方法,将播放进度返回出去. // 一定要掌握这种形式. [self.delegate getCurTiem:[self valueToString:[self getCurTime]] Totle:[self valueToString:[self getTotleTime]] Progress:[self getProgress]];}// 暂停方法-(void)musicPause{ [self.timer invalidate]; self.timer = nil; [self.player pause];}// 跳转方法-(void)seekToTimeWithValue:(CGFloat)value{ // 先暂停 [self musicPause]; // 跳转 [self.player seekToTime:CMTimeMake(value * [self getTotleTime], 1) completionHandler:^(BOOL finished) { if (finished == YES) { [self musicPlay]; } }];}// 获取当前的播放时间-(NSInteger)getCurTime{ if (self.player.currentItem) { // 用value/scale,就是AVPlayer计算时间的算法. 它就是这么规定的. // 下同. return self.player.currentTime.value / self.player.currentTime.timescale; } return 0;}// 获取总时长-(NSInteger)getTotleTime{ CMTime totleTime = [self.player.currentItem duration]; if (totleTime.timescale == 0) { return 1; }else { return totleTime.value /totleTime.timescale; }}// 获取当前播放进度-(CGFloat)getProgress{ return (CGFloat)[self getCurTime]/ (CGFloat)[self getTotleTime];}// 将整数秒转换为 00:00 格式的字符串-(NSString *)valueToString:(NSInteger)value{ return [NSString stringWithFormat:@"%.2ld:%.2ld",value/60,value%60];}// 返回一个歌词数组(这里有Bug)-(NSMutableArray *)getMusicLyricArray{ NSMutableArray * array = [NSMutableArray array]; for (NSString * str in self.model.timeLyric) { if (str.length == 0) { continue; } MusicLyricModel * model = [[MusicLyricModel alloc] init]; model.lyricTime = [str substringWithRange:NSMakeRange(1, 9)]; model.lyricStr = [str substringFromIndex:11]; [array addObject:model]; } return array;}-(NSInteger)getIndexWithCurTime{ NSInteger index = 0; NSString * curTime = [self valueToString:[self getCurTime]]; for (NSString * str in self.model.timeLyric) { if (str.length == 0) { continue; } if ([curTime isEqualToString:[str substringWithRange:NSMakeRange(1, 5)]]) { return index; } index ++; } return -1;}@end
关于通过代理返回播放进度:
- 一定要掌握这种形式, 在Block移植到OC之前, 多数三方SDK都是通过代理将回调, 包括传值和回调函数
四.数据模型Model类
4.1 歌曲信息model(E)
数据模型应该由后台服务端提供专门的文档, 这里不做赘述, 直接给出模型
新建一个类, 继承NSObject, 名称为: MusicInfoModel
MusicInfoModel.h
12345678910111213
#import <Foundation/Foundation.h>@interface MusicInfoModel : NSObject@property (nonatomic, strong) NSString *mp3Url;//音乐地址@property (nonatomic, strong) NSString *ID;// 歌曲ID (实际名称是id(小写的))@property (nonatomic, strong) NSString *name;//歌名@property (nonatomic, strong) NSString *picUrl;//图片地址@property (nonatomic, strong) NSString *blurPicUrl;//模糊图片地址@property (nonatomic, strong) NSString *album;//专辑@property (nonatomic, strong) NSString *singer;//歌手@property (nonatomic, strong) NSString *duration;//时长@property (nonatomic, strong) NSString *artists_name;//作曲@property (nonatomic, strong) NSArray *timeLyric;//歌词 (实际名称是lyric);@end
MusicInfoModel.m
123456789101112131415
#import "MusicInfoModel.h"@implementation MusicInfoModel// 重写的kvc部分方法.-(void)setValue:(id)value forUndefinedKey:(NSString *)key{ if ([key isEqualToString:@"id"]) { self.ID = value; } if ([key isEqualToString:@"lyric"]) { self.timeLyric = [value componentsSeparatedByString:@"\n"]; }}@end
4.2 歌词Model(F)
新建一个类, 继承NSObject, 名称为: MusicLyricModel
MusicLyricModel.h
123456
#import <Foundation/Foundation.h>@interface MusicLyricModel : NSObject@property (nonatomic, strong) NSString *lyricTime; //歌词时间@property (nonatomic, strong) NSString *lyricStr; //歌词@end
MusicLyricModel.m
1234
#import "MusicLyricModel.h"@implementation MusicLyricModel@end
数据模型部分不再赘述, 按照后台的文档来就可以了.
五.页面布局
5.1 歌曲列表(A)
在播放列表界面, 使用了自定义的cell的tableview, 我们对自定义的cell使用Xib布局.
新建一个类, 继承 UITableViewCell, 名称为: MusicListTableViewCell
MusicListTableViewCell.h
1234567891011
#import <UIKit/UIKit.h>#import "MusicInfoModel.h"@interface MusicListTableViewCell : UITableViewCell@property (weak, nonatomic) IBOutlet UIImageView *headImageView;@property (weak, nonatomic) IBOutlet UILabel *songNameLable;@property (weak, nonatomic) IBOutlet UILabel *authorNameLabel;@property(nonatomic,strong)MusicInfoModel * model;@end
MusicListTableViewCell.m
12345678910111213141516171819
#import "MusicListTableViewCell.h"@implementation MusicListTableViewCell// model的get方法, 外部一旦给model赋值了, 我们直接将model中的三个信息填到对应空间上.-(void)setModel:(MusicInfoModel *)model{ // self.headImageView.image = xxxx self.songNameLable.text = model.name; self.authorNameLabel.text = model.singer;}- (void)awakeFromNib { // Initialization code}- (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state}@end
XIB文件
5.2 歌曲列表(A)
在播放界面布局上, 较为复杂, 为了能够清晰展示我们的布局方案, 这里我们采取Frame布局.
新建一个类, 继承 UIView, 名称为: MusicPlayView
MusicPlayView.h
1234567891011121314151617181920212223
#import <UIKit/UIKit.h>@protocol MusicPlayViewDelegate <NSObject>-(void)lastSongAction;@end@interface MusicPlayView : UIView@property(nonatomic,strong)UIScrollView * mainScrollView;@property(nonatomic,strong)UIImageView * headImageView;@property(nonatomic,strong)UITableView * lyricTableView;@property(nonatomic,strong)UILabel * curTimeLabel;@property(nonatomic,strong)UISlider * progressSlider;@property(nonatomic,strong)UILabel * totleTiemLabel;@property(nonatomic,strong)UIButton * lastSongButton;@property(nonatomic,strong)UIButton * playPauseButton;@property(nonatomic,strong)UIButton * nextSongButton;@property(nonatomic,weak)id<MusicPlayViewDelegate>delegate;@end
MusicPlayView.m
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
#import "MusicPlayView.h"@implementation MusicPlayView// 初始化-(instancetype)init{ if (self = [super init]) { // 布局方法 [self p_setup]; self.backgroundColor = [UIColor whiteColor]; } return self;}-(void)p_setup{ // 1.ScorollView self.mainScrollView = [[UIScrollView alloc] init]; self.mainScrollView.frame = CGRectMake(0, 0,kScreenWidth , kScreenWidth); self.mainScrollView.contentSize = CGSizeMake(2*kScreenWidth, CGRectGetHeight(self.mainScrollView.frame)); self.mainScrollView.backgroundColor = [UIColor whiteColor]; self.mainScrollView.pagingEnabled = YES; self.mainScrollView.alwaysBounceHorizontal = YES; // 打开水平滚动 self.mainScrollView.alwaysBounceVertical = NO; // 关闭垂直滚动 [self addSubview:self.mainScrollView]; // 旋转的CD ImageView self.headImageView = [[UIImageView alloc] init]; self.headImageView.frame = CGRectMake(0, 0, kScreenWidth, CGRectGetHeight(self.mainScrollView.frame)); self.headImageView.backgroundColor = [UIColor redColor]; [self.mainScrollView addSubview:self.headImageView]; // 歌词tableView self.lyricTableView = [[UITableView alloc] initWithFrame:CGRectMake(kScreenWidth, 0, kScreenWidth, CGRectGetHeight(self.mainScrollView.frame)) style:(UITableViewStylePlain)]; [self.mainScrollView addSubview:self.lyricTableView]; // 当前播放时间 self.curTimeLabel = [[UILabel alloc] init]; self.curTimeLabel.frame = CGRectMake(CGRectGetMinX(self.mainScrollView.frame), CGRectGetMaxY(self.mainScrollView.frame), 60, 30); self.curTimeLabel.backgroundColor = [UIColor greenColor]; [self addSubview:self.curTimeLabel]; // 播放进度条 self.progressSlider = [[UISlider alloc] init]; self.progressSlider.frame = CGRectMake(CGRectGetMaxX(self.curTimeLabel.frame), CGRectGetMinY(self.curTimeLabel.frame), kScreenWidth - CGRectGetWidth(self.curTimeLabel.frame)*2, 30); [self addSubview:self.progressSlider]; // 总时间 self.totleTiemLabel = [[UILabel alloc] init]; self.totleTiemLabel.frame = CGRectMake(CGRectGetMaxX(self.progressSlider.frame), CGRectGetMinY(self.progressSlider.frame), CGRectGetWidth(self.curTimeLabel.frame), CGRectGetHeight(self.curTimeLabel.frame)); self.totleTiemLabel.backgroundColor = [UIColor greenColor]; [self addSubview:self.totleTiemLabel]; // 上一首的按钮 self.lastSongButton = [UIButton buttonWithType:(UIButtonTypeSystem)]; self.lastSongButton.frame = CGRectMake(CGRectGetMinX(self.curTimeLabel.frame), kScreenHeight - 30 - 94, 60, 30); self.lastSongButton.backgroundColor = [UIColor clearColor]; [self.lastSongButton setTitle:@"上一首" forState:(UIControlStateNormal)]; [self addSubview:self.lastSongButton]; [self.lastSongButton addTarget:self action:@selector(lastSongButtonAction:) forControlEvents:(UIControlEventTouchUpInside)]; // 下一首的按钮 self.nextSongButton = [UIButton buttonWithType:(UIButtonTypeSystem)]; self.nextSongButton.frame = CGRectMake(kScreenWidth - CGRectGetWidth(self.lastSongButton.frame), CGRectGetMinY(self.lastSongButton.frame), CGRectGetWidth(self.lastSongButton.frame), CGRectGetHeight(self.lastSongButton.frame)); self.nextSongButton.backgroundColor = [UIColor clearColor]; [self.nextSongButton setTitle:@"下一首" forState:(UIControlStateNormal)]; [self addSubview:self.nextSongButton]; // 播放/暂停的按钮 self.playPauseButton = [UIButton buttonWithType:(UIButtonTypeSystem)]; self.playPauseButton.frame = CGRectMake(kScreenWidth/2 - 30, CGRectGetMinY(self.lastSongButton.frame), CGRectGetWidth(self.lastSongButton.frame), CGRectGetHeight(self.lastSongButton.frame)); self.playPauseButton.backgroundColor = [UIColor clearColor]; [self addSubview:self.playPauseButton];}// 这里采用真正的MVC设计模式, 和其他的空间比较一下, 这里将lastButton的处理事件作为代理事件被外部重新实现.-(void)lastSongButtonAction:(UIButton *)sender{ [self.delegate lastSongAction];}@end
页面布局就这样吧, 写太多了, 好累
六.控制器–将Model和View结合起来, 添加控制逻辑!
6.1 歌曲列表(C)
直接上代码了, 没啥要说的. 注意代码注释.
新建一个类, 继承 UITableViewController, 名称为: MusicListTableViewController
MusicListTableViewController.h
12345
#import <UIKit/UIKit.h>@interface MusicListTableViewController : UITableViewController@end
MusicListTableViewController.m
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
#import "MusicListTableViewController.h"#import "MusicPlayViewController.h"@interface MusicListTableViewController ()@property(nonatomic,strong)NSArray * dataArray;@end@implementation MusicListTableViewController- (void)viewDidLoad { [super viewDidLoad]; [self.tableView registerNib:[UINib nibWithNibName:@"MusicListTableViewCell" bundle:nil] forCellReuseIdentifier:@"cell"]; // 调用获取播放列表的方法,结果已block的参数形式返回. [[GetDataTools shareGetData] getDataWithURL:kURL PassValue:^(NSArray *array) { // 花括号里面的代码,被称为block // block具有捕获当前上下文的功能.它能带着这个类中的dataArray,到另外一个类中去赋值. self.dataArray = array; // 花括号里的代码实际上再子线程中执行的. // 子线程中严禁更新UI. // 通过这种方式返回到主线程执行reloadata的操作. dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; }); }];}- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated.}#pragma mark - Table view data source- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return self.dataArray.count;}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MusicListTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; cell.model = self.dataArray[indexPath.row]; return cell;}-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 100;}-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ // 创建播放界面时, 我们使用的是单例方法. 也就是说我们的播放界面也做成了单例!!!!! MusicPlayViewController * MusicPlayVC =[MusicPlayViewController shareMusicPlay]; MusicPlayVC.index = indexPath.row; [self.navigationController pushViewController:MusicPlayVC animated:YES];}@end
6.2 播放界面控制器(D)
直接上代码了, 没啥要说的. 注意代码注释.
新建一个类, 继承 UIViewController, 名称为: MusicPlayViewController
MusicPlayViewController.h
123456
#import <UIKit/UIKit.h>@interface MusicPlayViewController : UIViewController// 将播放界面控制器设置成单例的方法@property(nonatomic,assign)NSInteger index;+(instancetype)shareMusicPlay;@end
MusicPlayViewController.m
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
#import "MusicPlayViewController.h"#import "MusicPlayView.h"@interface MusicPlayViewController ()<MusicPlayToolsDelegate,MusicPlayViewDelegate,UITableViewDataSource,UITableViewDelegate>@property(nonatomic,strong)MusicPlayView * rv;@property(nonatomic,strong)MusicPlayTools * aa;@property(nonatomic,strong)NSArray * lyricArray;@endstatic MusicPlayViewController * mp = nil;@implementation MusicPlayViewController-(void)loadView{ self.rv = [[MusicPlayView alloc]init]; self.view = _rv;}// 单例方法+(instancetype)shareMusicPlay{ if (mp == nil) { static dispatch_once_t once_token; dispatch_once(&once_token, ^{ mp = [[MusicPlayViewController alloc] init]; }); } return mp;}- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. // ios7以后,原点是(0,0)点, 而我们希望是ios7之前的(0,64)处,也就是navigationController导航栏的下面作为(0,0)点. 下面的设置就是做这个的. if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)]) { self.edgesForExtendedLayout = UIRectEdgeNone; } // 这里用一个指针指向播放器单例,以后使用这个单例的地方,可以直接使用这个指针,而不用每次都打印那么多. self.aa = [MusicPlayTools shareMusicPlay]; [MusicPlayTools shareMusicPlay].delegate = self; // 切割UIImageView为圆形. self.rv.headImageView.layer.cornerRadius = kScreenWidth / 2 ; self.rv.headImageView.layer.masksToBounds = YES; // 为View设置代理 self.rv.delegate = self; [self.rv.nextSongButton addTarget:self action:@selector(nextSongButtonAction:) forControlEvents:(UIControlEventTouchUpInside)]; [self.rv.progressSlider addTarget:self action:@selector(progressSliderAction:) forControlEvents:(UIControlEventValueChanged)]; [self.rv.playPauseButton addTarget:self action:@selector(playPauseButtonAction:) forControlEvents:(UIControlEventTouchUpInside)]; // 为播放器添加观察者,观察播放速率"rate". // 因为AVPlayer没有一个内部属性来标识当前的播放状态.所以我们可以通过rate变相的得到播放状态. // 这里观察播放速率rate,是为了获得播放/暂停的触发事件,作出相应的响应事件(比如更改button的文字). [self.aa.player addObserver:self forKeyPath:@"rate" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil]; // 设置歌词的tableView的代理 self.rv.lyricTableView.delegate = self; self.rv.lyricTableView.dataSource = self;}// 观察播放速率的相应方法: 速率==0 表示暂停.// 速率不为0 表示播放中.-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ if ([keyPath isEqualToString:@"rate"]) { if ([[change valueForKey:@"new"] integerValue] == 0) { [self.rv.playPauseButton setTitle:@"已经暂停" forState:(UIControlStateNormal)]; }else { [self.rv.playPauseButton setTitle:@"正在播放" forState:(UIControlStateNormal)]; } }}// 单例中,viewDidLoad只走一遍.切歌之类的操作需要多次进行,所以应该写在viewAppear中.// 每次出现一次页面都会尝试重新播放.-(void)viewWillAppear:(BOOL)animated{ [self p_play];}-(void)p_play{ // 判断当前播放器的model 和 点击cell的index对应的model,是不是同一个. // 如果是同一个,说明正在播放的和我们点击的是同一个, 这个时候不需要重新播放.直接返回就行了. if ([[MusicPlayTools shareMusicPlay].model isEqual:[[GetDataTools shareGetData] getModelWithIndex:self.index]]) { return; } // 如果播放中和我们点击的不是同一个,那么替换当前播放器的model. // 然后重新准备播放. [MusicPlayTools shareMusicPlay].model = [[GetDataTools shareGetData] getModelWithIndex:self.index]; // 注意这里准备播放 不是播放!!! [[MusicPlayTools shareMusicPlay] musicPrePlay]; // 设置歌曲封面 [self.rv.headImageView sd_setImageWithURL:[NSURL URLWithString:[MusicPlayTools shareMusicPlay].model.picUrl]]; // 将图片摆正 self.rv.headImageView.transform = CGAffineTransformMakeRotation(M_PI*2); // 设置歌词 self.lyricArray = [self.aa getMusicLyricArray]; [self.rv.lyricTableView reloadData];}- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated.}// 这个协议方法是播放器单例调起的.// 作为协议方法,播放器单例将播放进度已参数的形式传出来.-(void)getCurTiem:(NSString *)curTime Totle:(NSString *)totleTime Progress:(CGFloat)progress{ self.rv.curTimeLabel.text = curTime; self.rv.totleTiemLabel.text = totleTime; self.rv.progressSlider.value = progress; // 2d仿真变换. self.rv.headImageView.transform = CGAffineTransformRotate(self.rv.headImageView.transform, -M_PI/360); // 返回歌词在数组中的位置,然后根据这个位置,将tableView跳到对应的那一行. NSInteger index = [self.aa getIndexWithCurTime]; if (index == -1) { return; } NSIndexPath * tmpIndexPath = [NSIndexPath indexPathForRow:index inSection:0]; [self.rv.lyricTableView selectRowAtIndexPath:tmpIndexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle];}-(void)lastSongAction{ if (self.index > 0) { self.index --; }else{ self.index = [GetDataTools shareGetData].dataArray.count - 1; } [self p_play];}-(void)nextSongButtonAction:(UIButton *)sender{ if (self.index == [GetDataTools shareGetData].dataArray.count -1) { self.index = 0; }else { self.index ++; } [self p_play];}-(void)endOfPlayAction{ [self nextSongButtonAction:nil];}// 滑动slider-(void)progressSliderAction:(UISlider *)sender{ [[MusicPlayTools shareMusicPlay] seekToTimeWithValue:sender.value];}// 暂停播放方法-(void)playPauseButtonAction:(UIButton *)sender{ // 根据AVPlayer的rate判断. if ([MusicPlayTools shareMusicPlay].player.rate == 0) { [[MusicPlayTools shareMusicPlay] musicPlay]; }else { [[MusicPlayTools shareMusicPlay] musicPause]; }}-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return self.lyricArray.count;}-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:(UITableViewCellStyleDefault)reuseIdentifier:@"cell"]; } // 这里使用kvc取值,只是为了展示用,并不是必须用. cell.textLabel.text = [self.lyricArray[indexPath.row] valueForKey:@"lyricStr"]; return cell;}@end
容我喘口气, 说说为什么要将这个控制器设置成一个单例.
另附:
AppDelegate.m
1234567891011121314
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. self.window= [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; [self.window makeKeyAndVisible]; MusicListTableViewController * MusicListVC = [[MusicListTableViewController alloc] init]; UINavigationController * MusicListNC = [[UINavigationController alloc] initWithRootViewController:MusicListVC]; self.window.rootViewController = MusicListNC; return YES;}
pch文件
1234567891011121314
#import "MusicListTableViewController.h"#import "MusicListTableViewCell.h"#define kScreenWidth CGRectGetWidth([UIScreen mainScreen].bounds)#define kScreenHeight CGRectGetHeight([UIScreen mainScreen].bounds)#import "MusicInfoModel.h"#import "MusicLyricModel.h"#import "GetDataTools.h"#import "MusicPlayTools.h"// 这个接口就不公开了, 见谅#define kURL @"http://xxxx/MusicInfoList.plist"
- iOS开发--AVPlayer实现音乐播放器
- iOS开发--AVPlayer实现音乐播放器
- [IOS 开发] iOS音频篇:使用AVPlayer播放网络音乐
- [IOS 开发] iOS音频篇:使用AVPlayer播放网络音乐
- ios开发-AVPlayer 音乐播放自定义经典例题
- AVPlayer 实现视频播放器的开发
- iOS多媒体播放音乐AVAudioPlayer和AVPlayer
- iOS 开发:AVPlayer播放视频
- iOS开发:音乐播放器
- 音乐播放器 - iOS开发
- iOS AVPlayer播放器 简介
- IOS开发之AVPlayer(可定制播放器)
- iOS开发之AVPlayer(可定制播放器)
- iOS开发 - 用AVPlayer封装一个播放器
- iOS开发 - 用AVPlayer封装一个视频播放器
- iOS开发-使用AVAudioPlayer实现音乐播放器
- ios实现音乐播放器后台播放
- AVPlayer实现简单播放器
- DTW算法理解
- Volley框架的讲解
- Java中访问修饰符public、private、protect、default范围
- html语法基本结构
- MyAccount开发
- iOS开发--AVPlayer实现音乐播放器
- 设计模式--代理模式
- 面试中的html doctype到底是什
- Django学习笔记
- HDU 6011 Lotus and Characters
- 概率论(一)
- struts2控制文件上传和下载
- 【lintcode笔记】合并排序数组
- python+ffmpeg截取视频段