做一個(gè)完整的音樂播放器3.歌曲切換

上周遲到了,周末去參加OSC源創(chuàng)會(huì)了,還是有點(diǎn)啟發(fā)的壮莹。但這不是重點(diǎn)剑鞍,重點(diǎn)是 上一篇我只是實(shí)現(xiàn)了一首歌曲的在線播放,這肯定是不夠的铐然。這一篇博客主要是實(shí)現(xiàn)了多首歌曲的順序播放以及上一首和下一首切換蔬崩。
先看一下效果圖

效果圖.png

1.準(zhǔn)備工作

(1)數(shù)據(jù)源
?? 我把歌曲列表存在本地songList.json文件里恶座。用FHAlbumModel管理歌曲。
FHAlbumModel.h

#import <Foundation/Foundation.h>

@interface FHAlbumModel : NSObject

@property (nonatomic, copy) NSString *lrclink; // 歌詞
@property (nonatomic, copy) NSString *pic_big; // 背景圖
@property (nonatomic, copy) NSString *artist_name; // 歌手
@property (nonatomic, copy) NSString *title; // 歌名
@property (nonatomic, copy) NSString *song_id; // 歌曲地址


- (instancetype)initWithInfo: (NSDictionary *)InfoDic;
@end

FHAlbumModel.m

#import "FHAlbumModel.h"

@implementation FHAlbumModel

- (instancetype)initWithInfo: (NSDictionary *)InfoDic {
    
    FHAlbumModel *model = [[FHAlbumModel alloc] init];
   // 通過kvo為屬性賦值
    [model setValuesForKeysWithDictionary:InfoDic];
    return model;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    
}
@end

(2)聲明的變量

#import "FHMusicPlayerViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "UIColor+RGBHelper.h"
#import "FHCustomButton.h"
#import "Masonry.h"
#import "FHAlbumModel.h"
#import "FHLrcModel.h"

@interface FHMusicPlayerViewController ()<UITableViewDelegate, UITableViewDataSource>{
    
    UIImageView *_backImageView; // 背景圖
    UILabel *_album_titleLabel; // 標(biāo)題
    UILabel *_artist_nameLabel; // 副標(biāo)題
    UILabel *_currentLabel;  // 當(dāng)前時(shí)間
    UILabel *_durationLabel; // 總時(shí)間
    UIProgressView *_progressView; // 進(jìn)度條
    UISlider *_playerSlider;   // 播放控制器
    FHCustomButton *_playButton;  // 播放暫停
    FHCustomButton *_prevButton;  // 上一首
    FHCustomButton *_nextButton;  // 下一首
    BOOL _isPlay; // 記錄播放暫停狀態(tài)
    NSInteger _index; // 記錄播放到了第幾首歌
    FHAlbumModel *_currentModel;
    UITableView *_lrcTableView;  // 用于顯示歌詞
    int _row;  //記錄歌詞第幾行
}
@property (nonatomic, strong)NSMutableArray *albumArr; //歌曲
@property (nonatomic, strong)NSMutableArray *lrcArr;  // 歌詞
@property (nonatomic, strong)AVPlayer *avPlayer;
@property (nonatomic, strong)id timePlayProgerssObserver;// 播放器進(jìn)度觀察者

@end

UI的具體實(shí)現(xiàn)我就不一一介紹了沥阳,可以去我的GitUp下載源碼跨琳。只要記住每個(gè)變量的含義就好了,方便下面的觀看桐罕。
(3)懶加載變量

#pragma - mark 懶加載歌曲
- (NSMutableArray *)albumArr {
    
    if (!_albumArr) {
        
        _albumArr = [NSMutableArray new];
        // 從本地獲取json數(shù)據(jù)
        NSData *jsonData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"songList" ofType:@"json"]];
        // 把json數(shù)據(jù)轉(zhuǎn)換成字典
        NSDictionary *rootDic  = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:nil];
        NSArray *albumArr = [NSArray arrayWithArray:rootDic[@"song_list"]];
        for (NSDictionary *dic in albumArr) {
            FHAlbumModel *albumModel = [[FHAlbumModel alloc] initWithInfo:dic];
            [_albumArr addObject:albumModel];
        }
    }
    return _albumArr;
    
}
#pragma - mark 懶加載歌詞
- (NSMutableArray *)lrcArr{
    
    if (!_lrcArr) {
        _lrcArr = [NSMutableArray new];
    }
    return _lrcArr;
    
}
#pragma - mark 懶加載AVPlayer 
- (AVPlayer *)avPlayer {
    
    if (!_avPlayer) {
        AVPlayerItem *item = [AVPlayerItem new];
        _avPlayer = [[AVPlayer alloc] initWithPlayerItem:item];
    }
    return _avPlayer;
}

2.歌曲輪播

#pragma mark - 播放暫停
- (void)playAction:(UIButton *)button {
    
    _isPlay = !_isPlay;
    if (_isPlay) {
        _playButton.imageView.image = [UIImage imageNamed:@"play"];
        if (_currentModel) {
            [self.avPlayer play];
        }else {
            [self playMusic];
        }
      }else {
        _playButton.imageView.image = [UIImage imageNamed:@"stop"];
        [self.avPlayer pause];
    }
    
}

當(dāng)沒有歌曲播放時(shí)候脉让,添加歌曲。當(dāng)有歌曲播放時(shí)功炮,不添加歌曲溅潜。這樣可以保證暫停之后繼續(xù)播放。

- (void)playMusic {
   // 1.移除觀察者
   [self removeObserver];
   // 2.修改播放按鈕的圖片
   _playButton.imageView.image = [UIImage imageNamed:@"play"];
   // 3.獲取歌曲
   FHAlbumModel *albumModel = self.albumArr[_index];
   // 4.修改標(biāo)題
   _album_titleLabel.text = albumModel.title;
   // 5.修改副標(biāo)題
   _artist_nameLabel.text = [NSString stringWithFormat:@"%@ - 經(jīng)典老歌榜",albumModel.artist_name];
   // 6. 實(shí)例化新的playerItem
   AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:albumModel.song_id]];
   // 7.取代舊的playerItem
   [self.avPlayer replaceCurrentItemWithPlayerItem:playerItem];
   // 8.開始播放
   [self.avPlayer play];
   // 9.添加緩存狀態(tài)的觀察者
   [self addObserverOfLoadedTimeRanges];
   // 10.添加播放進(jìn)度的觀察者
   [self addTimePlayProgerssObserver];
   // 11.記錄當(dāng)前播放的歌曲
   _currentModel = self.albumArr[_index];
   // 12.獲取歌詞
   [self getAlbumLrc];
}
```
**分析**:1.**添加觀察者之前需要把以前的觀察者移除**薪伏。如果不移除self.avPlayer.currentItem 的觀察者滚澜,就會(huì)報(bào)“An instance 0x174009380 of class AVPlayerItem was deallocated while key value observers were still registered with it”。意思是觀察的對(duì)象已經(jīng)釋放嫁怀,還對(duì)它進(jìn)行觀察设捐。我們切換歌曲時(shí),原來的歌曲對(duì)象已經(jīng)釋放了塘淑,所以對(duì)原來歌曲對(duì)象添加的觀察者也應(yīng)該移除萝招;雖然self.avPlayer一直存在,但是如果對(duì)它一直添加觀察者存捺,會(huì)耗費(fèi)大量?jī)?nèi)存即寒,為了防止內(nèi)存溢出所以也應(yīng)該移除。
```
#pragma mark - 移除觀察者
- (void)removeObserver {
// 沒添加之前不能移除否則會(huì)崩潰
   if (!_currentModel) {
       return;
   }else {
        [self.avPlayer.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
       [self.avPlayer removeTimeObserver:self.timePlayProgerssObserver];
   }
}
```
```
#pragma mark - 監(jiān)聽緩存狀態(tài) 
- (void)addObserverOfLoadedTimeRanges {
   
   [self.avPlayer.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
   
   if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
       NSArray * timeRanges = self.avPlayer.currentItem.loadedTimeRanges;
       //本次緩沖的時(shí)間范圍
       CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
       //緩沖總長(zhǎng)度
       NSTimeInterval totalLoadTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration);
       //音樂的總時(shí)間
       NSTimeInterval duration = CMTimeGetSeconds(self.avPlayer.currentItem.duration);
       //計(jì)算緩沖百分比例
       NSTimeInterval scale = totalLoadTime/duration;
       //更新緩沖進(jìn)度條
       _progressView.progress = scale;
       
       _durationLabel.text = [NSString stringWithFormat:@"%d:%@",(int)duration/60,[self FormatTime:(int)duration%60]];
   }
}
```
```
#pragma mark - 添加播放進(jìn)度的觀察者
- (void)addTimePlayProgerssObserver {
   
   __block UISlider *weakPregressSlider = _playerSlider;
   __weak UILabel *waekCurrentLabel = _currentLabel;
   __block int weakRow = _row;
   __weak typeof(self) weakSelf = self;
   self.timePlayProgerssObserver = [self.avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
       
       // 當(dāng)前播放的時(shí)間
       float current = CMTimeGetSeconds(time);
       // 更新歌詞
       if (weakRow < weakSelf.lrcArr.count) {
           FHLrcModel *model = weakSelf.lrcArr[weakRow];
           if (model.presenTime == (int)current) {
               [weakSelf reloadTabelViewWithRow:weakRow];
               weakRow++;
           }
       }
       // 總時(shí)間
       float total = CMTimeGetSeconds(weakSelf.avPlayer.currentItem.duration);
       // 更改當(dāng)前播放時(shí)間
       NSString *currentSStr = [weakSelf FormatTime: (int)current % 60];
       waekCurrentLabel.text = [NSString stringWithFormat:@"%d:%@",(int)current / 60,currentSStr];
       // 更新播放進(jìn)度條
       weakPregressSlider.value = current / total;
           
   }];
}
```
```
 // 播放完成通知
   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nextButtonClick:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
```
??寫在viewDidLoad里召噩,因?yàn)樘砑右淮尉涂梢阅刚浴2シ磐瓿芍苯硬シ畔乱皇住?```
#pragma mark - 上一首
- (void)prevButtonClick :(UIButton *)button {
   _index--;
   if (_index < 0) {
     
       _index = self.albumArr.count - 1;
   }
   [self playMusic];
}
#pragma mark - 下一首
- (void)nextButtonClick :(UIButton *)button {
   _index++;
       if (_index >= self.albumArr.count) {
       
       _index = 0;
   }
   [self playMusic];
}
```
??當(dāng)播放第一首歌曲時(shí),點(diǎn)擊上一首播放最后一首歌曲具滴。當(dāng)播放最后一首歌曲時(shí)凹嘲,點(diǎn)擊下一首播放第一首歌曲。
??由于篇幅的原因构韵,下一篇博客再介紹歌詞的實(shí)現(xiàn)周蹭。重要的事情說三遍:項(xiàng)目地址[GitUp](https://github.com/haichong/iOSPlayerStudy) ,歡迎下載疲恢。

















??
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凶朗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子显拳,更是在濱河造成了極大的恐慌棚愤,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異宛畦,居然都是意外死亡瘸洛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門次和,熙熙樓的掌柜王于貴愁眉苦臉地迎上來反肋,“玉大人,你說我怎么就攤上這事踏施∈幔” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵畅形,是天一觀的道長(zhǎng)养距。 經(jīng)常有香客問我,道長(zhǎng)束亏,這世上最難降的妖魔是什么铃在? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任阵具,我火速辦了婚禮碍遍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘阳液。我一直安慰自己怕敬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布帘皿。 她就那樣靜靜地躺著东跪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鹰溜。 梳的紋絲不亂的頭發(fā)上虽填,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音曹动,去河邊找鬼斋日。 笑死,一個(gè)胖子當(dāng)著我的面吹牛墓陈,可吹牛的內(nèi)容都是我干的恶守。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼贡必,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼兔港!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起仔拟,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤衫樊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后利花,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橡伞,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盒揉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了兑徘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刚盈。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖挂脑,靈堂內(nèi)的尸體忽然破棺而出藕漱,到底是詐尸還是另有隱情,我是刑警寧澤崭闲,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布肋联,位于F島的核電站,受9級(jí)特大地震影響刁俭,放射性物質(zhì)發(fā)生泄漏橄仍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一牍戚、第九天 我趴在偏房一處隱蔽的房頂上張望侮繁。 院中可真熱鬧,春花似錦如孝、人聲如沸宪哩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)锁孟。三九已至,卻和暖如春茁瘦,著一層夾襖步出監(jiān)牢的瞬間品抽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工甜熔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留圆恤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓纺非,卻偏偏與公主長(zhǎng)得像哑了,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子烧颖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容