iOS开发--AVPlayer实现音乐播放器

来源:互联网 发布:淘宝厂家直销违规吗 编辑:程序博客网 时间:2024/05/01 23:49
 

iOS开发--AVPlayer实现音乐播放器 

标签: url音乐前端开发多线程
 5190人阅读 评论(2) 收藏 举报

目录(?)[+]

这是一篇教学Blog. 重点不完全在播放器上, 目的是通过这个过程掌握以下知识点:

  • 单例
  • block传值
  • 多线程
  • 代理传值
  • 通知
  • 观察者
  • 网络请求
  • 数据解析
  • 多控件布局
  • 开发模式和框架设计

今天敲一个音乐播放器, 音乐源我就不共享了, 涉及到版权保护, 别问我的源是哪儿来的. 不告诉你们

这篇博客是一篇教学Blog, Demo不能直接用作生产, 但其中的逻辑是经得起推敲, UI部分美化美化一下即可. 要做到举一反三.

开始敲之前, 我们先看看当前可供使用的多媒体播放框架有哪些

file-list


简单介绍一下:

  • 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播放器的区别在于, 这货只能播放本地音乐.

另附一张表格, 里面登记了大多数播放器的优缺点, 图片来源网络:
file-list

下面开始我们的音乐播放器之旅.

一. 产品原型图

当我们在实际生产过程中, 作为App前端开发工程师, 我们会拿到产品模型(原型图), 这个模型可能使用墨刀为你精准绘制, 也可能某个页面使用草纸为你勾勒, 不管怎样, 你肯定能拿到下面的东西, 这些图片,描绘了你要做的app大概长成什么样子.

  • 歌曲列表
    file-list

  • 播放界面
    file-list

  • 播放界面滑动CD还有歌词呢
    file-list

我们要做的就是上面样式的播放器, 如果您觉得太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开始, 深入浅出, 然后深入, 深入, 再深入.

file-list



三.工具类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文件
file-list

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"

0 0
原创粉丝点击