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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>
// 定义block
typedef 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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

1
2
3
4
5
6
#import <Foundation/Foundation.h>

@interface MusicLyricModel : NSObject
@property (nonatomic, strong) NSString *lyricTime; //歌词时间
@property (nonatomic, strong) NSString *lyricStr;  //歌词
@end

MusicLyricModel.m

1
2
3
4
#import "MusicLyricModel.h"

@implementation MusicLyricModel
@end

数据模型部分不再赘述, 按照后台的文档来就可以了.

五.页面布局

5.1 歌曲列表(A)

在播放列表界面, 使用了自定义的cell的tableview, 我们对自定义的cell使用Xib布局.

新建一个类, 继承 UITableViewCell, 名称为: MusicListTableViewCell

MusicListTableViewCell.h

1
2
3
4
5
6
7
8
9
10
11
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#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

1
2
3
4
5
#import <UIKit/UIKit.h>

@interface MusicListTableViewController : UITableViewController

@end

MusicListTableViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#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

1
2
3
4
5
6
#import <UIKit/UIKit.h>
@interface MusicPlayViewController : UIViewController
// 将播放界面控制器设置成单例的方法
@property(nonatomic,assign)NSInteger index;
+(instancetype)shareMusicPlay;
@end

MusicPlayViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#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;
@end

static 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (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文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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"

这是一篇教学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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>
// 定义block
typedef 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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

1
2
3
4
5
6
#import <Foundation/Foundation.h>

@interface MusicLyricModel : NSObject
@property (nonatomic, strong) NSString *lyricTime; //歌词时间
@property (nonatomic, strong) NSString *lyricStr;  //歌词
@end

MusicLyricModel.m

1
2
3
4
#import "MusicLyricModel.h"

@implementation MusicLyricModel
@end

数据模型部分不再赘述, 按照后台的文档来就可以了.

五.页面布局

5.1 歌曲列表(A)

在播放列表界面, 使用了自定义的cell的tableview, 我们对自定义的cell使用Xib布局.

新建一个类, 继承 UITableViewCell, 名称为: MusicListTableViewCell

MusicListTableViewCell.h

1
2
3
4
5
6
7
8
9
10
11
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#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

1
2
3
4
5
#import <UIKit/UIKit.h>

@interface MusicListTableViewController : UITableViewController

@end

MusicListTableViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#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

1
2
3
4
5
6
#import <UIKit/UIKit.h>
@interface MusicPlayViewController : UIViewController
// 将播放界面控制器设置成单例的方法
@property(nonatomic,assign)NSInteger index;
+(instancetype)shareMusicPlay;
@end

MusicPlayViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#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;
@end

static 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (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文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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"

猜你喜欢

转载自blog.csdn.net/wakice/article/details/56057758