02.音視頻播放 QQ音樂界面搭建

@(iOS Study)[音視頻播放]


目錄

  • 02.音視頻播放 QQ音樂界面搭建 (自己整理)
  • QQ音樂界面搭建
    • 1.storyboard布局QQ音樂界面
      • QQ音樂主界面整體框圖
      • 界面控件設置
    • 2.實現音樂的播放
      • 封裝WXAudioTool播放音效和播放音樂工具類
      • 播放音效/音樂工具類實現步驟
      • 封裝WXMusicTool獲取音樂列表工具類
      • 在主控制器實現播放音樂
    • 3.添加播放進度定時器,更新播放進度
      • 實現更新播放進度功能和歌手圖標旋轉功能
      • 實時更新播放進度信息
      • 歌手圖標旋轉動畫
    • 4.處理滾動條
      • 自定義滾動歌詞的WXLrcScrollView,繼承UIScrollView
    • 5.播放/暫停,上一首,下一首功能實現,音樂播放完畢自動切換到下一首
      • 播放/暫停,上一首,下一首功能實現
      • 音樂播放完畢自動切換到下一首
    • 6.設置進度條UISlider的處理
      • 監聽進度條的事件
      • 進度條添加Tap敲擊手勢
    • 7.歌詞的解析
      • 歌詞模型類功能的實現
      • 創建歌詞文件處理WXLrcTool工具類
    • 8.實現歌詞ScrollView歌詞的滾動功能
      • 主界面控制器創建更新歌詞的定時器CADisplayLink定時器
      • 自定義顯示歌詞的WXLrcLabel
      • 實現歌詞的滾動,當前行歌詞進度顏色填充效果
    • 10.設置主界面的歌詞
      • 在主界面中設置主界面歌詞
      • 由lrcScrollView內部為主界面歌詞內容和進度進行更新
    • 11.實現鎖屏界面信息展示和操作
      • 鎖屏界面項目配置
    • 12.實現鎖屏鎖屏歌詞展示
      • 繪制鎖屏封面和歌詞
      • 鎖屏界面實現播放,暫停,上一首,下一首功能

QQ音樂界面搭建

  • QQ音樂運行效果(模擬器不能演示鎖屏界面的功能,所以展示效果圖沒有鎖屏界面功能展示)
1.QQ音樂運行效果.gif

1.storyboard布局QQ音樂界面

QQ音樂主界面整體框圖

  • 界面效果圖
2.QQ音樂主界面整體框圖.png
  • UISlider添加手勢
3.UISlider添加手勢.png
  • UILabel作為其他控件布局的參考控件注意點
    • 如果UILabel的(頂部Top或底部Bottom)作為其他控件布局的參考對象的時候,需對設置UILabel的高度約束. 高度約束如下圖所示
4.高度約束.png

界面控件設置

  • 設置毛玻璃效果的幾種方式
    • 美工做一張毛玻璃效果的圖片
    • 使用UIToolbar
    • 使用第三方框架
    • 使用coreImage
    • 使用UIVisualEffectView (Blur)
5.使用UIVisualEffectView (Blur).png
  • 設置背景毛玻璃效果
    • 本示例使用UIToolbar做毛玻璃效果
    • 示例代碼
/** 給背景添加毛玻璃效果 */
- (void)setupBlur
{
    // REMARKS: 添加毛玻璃效果
    // 1.創建toolBar,設置毛玻璃樣式,添加到背景的view
    UIToolbar *toolBar = [[UIToolbar alloc] init];
    toolBar.barStyle = UIBarStyleBlack;
    [self.albumView addSubview:toolBar];
    
    // 2.toolBar添加約束
    [toolBar mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.albumView);
    }];
}
  • 設置歌手圖標圓角效果
    • view即將布局子控件的時候調用viewWillLayoutSubviews方法,該方法中可以獲取子控件的真實尺寸
// SINGLE: view即將布局子控件的時候調用,在該方法中可以拿到子控件的真實尺寸
- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    
    // SINGLE: 設置ImageView為圓形
    self.iconView.layer.cornerRadius = self.iconView.frame.size.width * 0.5;
    self.iconView.layer.masksToBounds = YES;
    // SINGLE: 設置ImageView邊框顏色,寬度
    self.iconView.layer.borderColor = [UIColor lightGrayColor].CGColor;
    self.iconView.layer.borderWidth = 5.0;
}
  • 設置UISlider滾動條滑塊圖片
// SINGLE: 2.設置UISlider滑塊圖片
[self.progressSlider setThumbImage:[UIImage imageNamed:@"player_slider_playback_thumb"] forState:UIControlStateNormal];

2.實現音樂的播放

封裝WXAudioTool播放音效和播放音樂工具類

播放音效/音樂工具類實現步驟

  • 1.創建一個可變字典用于存放音樂播放器AVAudioPlayer,創建一個可變字典用于存放播放音效SystemSoundID,作為音樂播放器和音效播放的內存緩存.
    • 音樂播放器和音效播放器緩存只需創建一次,所以將其放在initialize方法中進行初始化操作.示例代碼如下
#pragma mark - 初始化設置
/** 創建內存緩存 */
+ (void)initialize {
// 創建可變字典,用于存放播放音效SystemSoundID
_soundIDs = [NSMutableDictionary dictionary];

// 創建可變字典,用于存放音樂播放器AVAudioPlayer
_players = [NSMutableDictionary dictionary];
}
  • 2.WXAudioTool工具類實現播放等API接口方法

    • 實現播放音樂,暫停音樂,停止音樂和播放音效API接口方法

    播放音樂: + (AVAudioPlayer *)playMusicWithMusicName:(NSString *)musicName;
    暫停音樂: + (void)pauseMusicWithMusicName:(NSString *)musicName;
    停止音樂: + (void)stopMusicWithMusicName:(NSString *)musicName;
    播放音效: + (void)playSoundWithSoundName:(NSString *)soundName;

  • 3.WXAudioTool工具類實現代碼

#import "WXAudioTool.h"
#import <AVFoundation/AVFoundation.h>

@implementation WXAudioTool

// SINGLE: 創建一個可變字典緩存音樂,字典只需創建一次,可以在initialize類方法中創建
static NSMutableDictionary *_soundIDs;
// SINGLE: 創建音樂內存緩存,在initialize類方法中創建
static NSMutableDictionary *_players;

#pragma mark - 初始化設置
/** 創建內存緩存 */
+ (void)initialize {
    // 創建可變字典,用于存放播放音效SystemSoundID
    _soundIDs = [NSMutableDictionary dictionary];
    
    // 創建可變字典,用于存放音樂播放器AVAudioPlayer
    _players = [NSMutableDictionary dictionary];
}


#pragma mark - 播放音樂API(AVAudioPlayer)
/** 播放音樂 */
+ (AVAudioPlayer *)playMusicWithMusicName:(NSString *)musicName
{
    // 1.先從內存字典中獲取播放器AVAudioPlayer
    AVAudioPlayer *player = _players[musicName];
    
    // 2.判斷是否從內存獲取到播放器,如果沒有獲取到,新建播放器
    if (player == nil) {
        // 2.1 獲取音樂文件的url
        NSURL *url = [[NSBundle mainBundle] URLForResource:musicName withExtension:nil];
        if (url == nil) return nil;

        // 2.2 根據音頻文件的url,創建播放器
        player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
        
        // 2.3 保存到內存緩存_players
        [_players setObject:player forKey:musicName];
    }
    
    // SINGLE: 3.播放音樂
    [player play];
    
    return player;
}

/** 暫停音樂 */
+ (void)pauseMusicWithMusicName:(NSString *)musicName
{
    // 1.從內存字典中取出播放器
    AVAudioPlayer *player = _players[musicName];
    
    // 2.如果內存中有獲取到播放器,暫停播放
    if (player) {
        // SINGLE: 暫停播放
        [player pause];
    }
}

/** 停止音樂 */
+ (void)stopMusicWithMusicName:(NSString *)musicName
{
    // 1.從內存字典中取出播放器
    AVAudioPlayer *player = _players[musicName];
    
    // 2.如果內存中有獲取到播放器,停止播放
    if (player) {
        // SINGLE: 2.1 停止播放
        [player stop];
        // 2.2 從內存字典中移除
        [_players removeObjectForKey:musicName];
        player = nil;
    }
}


#pragma mark - 播放短音效API(SystemSoundID)

// REMARKS: 播放音效類方法
/** 播放音效 */
+ (void)playSoundWithSoundName:(NSString *)soundName
{
    // 1.先從內存緩存獲取soundID
    SystemSoundID soundID = [_soundIDs[soundName] unsignedIntValue];
    
    // 2.判斷內存是否存在音效資源,內存沒有音效資源,則創建
    if (soundID == 0) {
        // 2.1 若不存在, 創建音效資源url
        CFURLRef url = (__bridge CFURLRef)[[NSBundle mainBundle] URLForResource:soundName withExtension:nil];
        
        // 2.2 判斷url是否為空,如果為空,說明資源不存在,直接退出
        if (url == nil) return;
        
        // 2.3 生成SystemSoundID
        AudioServicesCreateSystemSoundID(url, &soundID);
        
        // 2.4 存入可變字典內存緩存
        [_soundIDs setObject:@(soundID) forKey:soundName];
    }
    
    // 3.播放音效
    AudioServicesPlaySystemSound(soundID);
}

@end

封裝WXMusicTool獲取音樂列表工具類

  • 1.創建WXMusicItem音樂模型
@interface WXMusicItem : NSObject
/** 音樂名 */
@property (nonatomic ,copy)NSString *name;
/** 音樂文件名 */
@property (nonatomic ,copy)NSString *filename;
/** 歌詞文件名 */
@property (nonatomic ,copy)NSString *lrcname;
/** 歌手名 */
@property (nonatomic ,copy)NSString *singer;
/** 歌手小圖標 */
@property (nonatomic ,copy)NSString *singerIcon;
/** 歌手大圖標 */
@property (nonatomic ,copy)NSString *icon;
@end
  • 2.WXMusicTool工具類實現API接口方法

    • 實現播放音樂,暫停音樂,停止音樂和播放音效API接口方法
      獲取所有的音樂: + (NSArray *)musics;
      獲取正在播放的音樂: + (WXMusicItem *)playingMusic;
      設置播放的音樂: + (void)setupMusic:(WXMusicItem *)music;
      獲取上一首音樂: + (WXMusicItem *)previous;
      獲取下一首音樂: + (WXMusicItem *)next;
  • 3.WXMusicTool工具類實現代碼

#import "WXMusicTool.h"
#import "WXMusicItem.h"
#import <MJExtension.h>

@implementation WXMusicTool

#pragma mark - 靜態變量
/** 所有音樂 */
static NSArray *_musicItems;
/** 當前播放的音樂 */
static WXMusicItem *_playingMusicItem;

/** 獲取所有音樂模型,設置當前默認音樂 */
+ (void)initialize
{
    // 從plist文件中獲取所有音樂模型
    _musicItems = [WXMusicItem mj_objectArrayWithFilename:@"Musics.plist"];
    // 設置當前默認音樂
    _playingMusicItem = _musicItems[4];
}

#pragma mark - 播放音樂操作
/** 獲取所有的音樂 */
+ (NSArray *)musics
{
    return _musicItems;
}

/** 獲取正在播放的音樂(默認) */
+ (WXMusicItem *)playingMusic
{
    return _playingMusicItem;
}

/** 設置播放的音樂 */
+ (void)setupMusic:(WXMusicItem *)music
{
    _playingMusicItem = music;
}

/** 獲取上一首音樂 */
+ (WXMusicItem *)previous
{
    // 1.獲取當前音樂的下標值
    NSInteger currentIndex = [_musicItems indexOfObject:_playingMusicItem];
    
    // 2.獲取上一首音樂的下標值,判斷是否越界
    NSInteger previousIndex = currentIndex - 1;
    if (previousIndex < 0) {
        previousIndex = _musicItems.count - 1;
    }
    
    // 3.獲取上一首的音樂
    WXMusicItem *previousMusicItem = _musicItems[previousIndex];
    
    // 4.返回上一首音樂
    return previousMusicItem;
}

/** 獲取下一首音樂 */
+ (WXMusicItem *)next
{
    // 1.獲取當前音樂的下標值
    NSInteger currentIndex = [_musicItems indexOfObject:_playingMusicItem];
    
    // 2.獲取下一首音樂的下標值,判斷是否越界
    NSInteger nextIndex = currentIndex + 1;
    if (nextIndex >= _musicItems.count) {
        nextIndex = 0;
    }
    
    // 3.獲取下一首的音樂
    WXMusicItem *nextMusicItem = _musicItems[nextIndex];
    
    // 4.返回下一首音樂
    return nextMusicItem;
}
@end

在主控制器實現播放音樂

  • 播放音樂實現
#pragma mark - 播放音樂
- (void)playingMusic
{
    // 1.獲取當前音樂
    WXMusicItem *playerMusicItem = [WXMusicTool playingMusic];
    
    // 2.更新子控件信息
    self.albumView.image = [UIImage imageNamed:playerMusicItem.icon];
    self.iconView.image = [UIImage imageNamed:playerMusicItem.icon];
    self.songLabel.text = playerMusicItem.name;
    self.singerLabel.text = playerMusicItem.singer;
    
    // 3.開始播放音樂
    AVAudioPlayer *currentPlayer = [WXAudioTool playMusicWithMusicName:playerMusicItem.filename];
    // 3.0 設置代理,用來監聽音樂播放完畢,實現自動切換到下一首的功能
    currentPlayer.delegate = self;
    self.currentPlayer = currentPlayer;
    // 3.1 設置當前播放時間和音樂總時長
    self.currentLabel.text = [NSString stringWithTime:currentPlayer.currentTime];
    self.totalLabel.text = [NSString stringWithTime:currentPlayer.duration];
    // 3.2 更新當前播放按鈕的狀態
    self.playOrPauseBtn.selected = self.currentPlayer.isPlaying;
    // 3.3 設置當前播放的音樂的歌詞
    self.lrcScrollView.lrcFileName = playerMusicItem.lrcname;
    // 3.4 將當前播放的音樂的總時長傳給lrcScrollView,用于做鎖屏界面的總時長
    self.lrcScrollView.duration = currentPlayer.duration;
    
    // 4.添加旋轉動畫
    [self addIconViewAnimate];
    
    // 5.添加定時器(需先移除定時器在添加,避免當前定時器還在運行,又開啟新定時器)
    [self removeProgressTimer];
    [self addProgressTimer];
    
    // 6.添加更新歌詞定時器
    [self removeLrcTimer];
    [self addLrcTimer];
    
    // 7.設置默認當前音樂播放時間,總時長和進度條
    [self updateProgressInfo];
}

3.添加播放進度定時器,更新播放進度

1.添加定時器步驟,需先移除當前定時器,再添加定時器.
2.將定時器添加到NSRunLoop,并設置NSRunLoopCommonModes模式.
3.歌手圖標旋轉動畫實現,使用基礎核心動畫CABasicAnimation,設置繞z軸旋轉360°無限循環等動畫屬性配置.

實現更新播放進度功能和歌手圖標旋轉功能

  • 播放進度定時器創建/移除方法
    • 創建播放進度定時器: - (void)addProgressTimer;
    • 移除播放進度定時器: - (void)removeProgressTimer;

實時更新播放進度信息

  • 更新進度進度實現
- (void)updateProgressInfo {
    // 1.更新當前音樂播放時間和當前音樂總時長
    self.currentLabel.text = [NSString stringWithTime:self.currentPlayer.currentTime];
    self.totalLabel.text = [NSString stringWithTime:self.currentPlayer.duration];
    
    // 2.更新進度條信息
    self.progressSlider.value = self.currentPlayer.currentTime / self.currentPlayer.duration;
}

歌手圖標旋轉動畫

  • 歌手圖標旋轉動畫實現
- (void)addIconViewAnimate
{
    // 1.創建核心動畫,并設置期相關屬性
    CABasicAnimation *anim = [CABasicAnimation animation];
    // 1.1 設置繞z軸旋轉360°,無限循環等
    anim.keyPath = @"transform.rotation.z";
    anim.fromValue = @(0);
    anim.toValue = @(M_PI * 2);
    anim.repeatCount = CGFLOAT_MAX;
    anim.duration = 35;
    // SINGLE: 1.2 當進入后臺,再進入前臺時,核心動畫會失效,需設置removedOnCompletion屬性為NO,這樣核心動畫就不會失效
    // removedOnCompletion: 設置為NO表示動畫完成的時候不要移除.
    anim.removedOnCompletion = NO;
    
    // 2.添加動畫到self.iconView.layer
    [self.iconView.layer addAnimation:anim forKey:nil];
}

4.處理滾動條

自定義滾動歌詞的WXLrcScrollView,繼承UIScrollView

  • 1.在WXLrcScrollView初始化時,添加滾動歌詞的tableView
    • initWithCoderinitWithFrame方法中開啟分頁功能,初始化和添加tableView到WXLrcScrollView
    • 設置子控件tableView的數據源和代理,實現數據源方法,為tableView提供顯示的測試數據
    • 在layoutSubviews中添加tableView的約束,并設置tableView背景顏色,分割線,內邊距等,必須在layoutSubviews中設置,否則會tableView中的歌詞播放不同步,背景為白色等問題.
/** layoutSubviews中布局子控件tableView */
- (void)layoutSubviews
{
    [super layoutSubviews];
    // 1.布局tableView
    [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.mas_top);
        make.bottom.equalTo(self.mas_bottom);
        make.height.equalTo(self.mas_height);
        make.left.equalTo(self.mas_left).offset(self.bounds.size.width);
        make.right.equalTo(self.mas_right);
        make.width.equalTo(self.mas_width);
    }];
    
    // SINGLE: 2.清空tableView背景顏色,取消tableView的分割線,設置tableView的內邊距
    self.tableView.backgroundColor = [UIColor clearColor];
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    self.tableView.contentInset = UIEdgeInsetsMake(self.tableView.bounds.size.height * 0.5, 0, self.tableView.bounds.size.height * 0.5, 0);
}

5.播放/暫停,上一首,下一首功能實現,音樂播放完畢自動切換到下一首

在storyboard中設置播放按鈕在Normal/Selected狀態下的按鈕顯示的圖片

播放/暫停,上一首,下一首功能實現

  • 播放/暫停功能實現

    • 切換播放按鈕的狀態
    • 判斷是否正在播放,如果當前正在播放則暫停播放,反之則繼續播放
      • 暫停播放: 暫停播放,移除定時器,暫停動畫(分類實現暫停和繼續動畫)
      • 繼續播放: 繼續播放,開啟定時器,繼續動畫
  • 上一首/下一首功能實現

    • 使用WXMusicTool工具類獲取上一首/下一首音樂
    • 實現切換音樂的方法
  • 示例代碼

/** 下一首 */
- (IBAction)nextMusic {
    
    // 1.獲取下一首音樂
    WXMusicItem *nextMusicItem = [WXMusicTool next];
    
    // 2.播放下一首音樂
    [self playMusic:nextMusicItem];
    
}
/** 上一首 */
- (IBAction)previousMusic {
    // 1.獲取上一首音樂
    WXMusicItem *previousMusicItem = [WXMusicTool previous];
    
    // 2.播放上一首音樂
    [self playMusic:previousMusicItem];
}
/** 切換播放的音樂 */
- (void)playMusic:(WXMusicItem *)music {
    // 1.獲取當前音樂,并暫停播放
    WXMusicItem *curMusicItem = [WXMusicTool playingMusic];
    [WXAudioTool pauseMusicWithMusicName:curMusicItem.filename];
    
    // 2.設置當前播放音樂music
    [WXMusicTool setupMusic:music];
    
    // 3.開始播放音樂
    [self playingMusic];
}

音樂播放完畢自動切換到下一首

  • 主控制器的當前播放器遵守AVAudioPlayerDelegate代理協議
  • 在播放器創建完成時設置代理,用來監聽音樂播放完畢,實現自動切換到下一首的功能
    self.currentPlayer.delegate = self;
  • 實現代理方法audioPlayerDidFinishPlaying:successfully,當前音樂正常播放完成后調用.
#pragma mark - <AVAudioPlayerDelegate>代理協議
// SINGLE: 當前音樂播放完成后調用,在<AVAudioPlayerDelegate>代理協議中
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
    // flag == YES 表示音樂播放正常停止,播放完畢自動切換到下一首
    if (flag) {
        // 音樂播放完畢自動切換下一首
        [self nextMusic];
    }
}

6.設置進度條UISlider的處理

監聽進度條的事件

  • 監聽進度條TouchDown事件,當UISlider監聽到TouchDown事件時,移除進度條定時器
  • 監聽進度條TouchUpInside事件,當UISlider監聽到TouchUpInside事件時,更新當前播放進度,并重新開啟定時器
  • 監聽到進度條ValueChange事件,拖動UISlider時,使用UISlider的value屬性和當前音樂總時長計算當前播放進度,并更新當前播放時間Label的文字信息.
    self.currentLabel.text = [NSString stringWithTime:self.progressSlider.value * self.currentPlayer.duration];

進度條添加Tap敲擊手勢

  • 在Main.storyboard給UISlider進度條添加點擊Tap手勢
6.進度條添加Tap敲擊手勢.png
  • 監聽敲擊手勢代碼實現
/** 監聽進度條點擊Tap手勢 */
- (IBAction)sliderTap:(UITapGestureRecognizer *)sender {
    // 1.獲取當前點的位置
    CGPoint curPoint = [sender locationInView:sender.view];
    
    // 2.獲取當前點與總進度的比例
    CGFloat ratio = curPoint.x / self.progressSlider.bounds.size.width;
    
    // 3.更新當前播放器的播放時間
    self.currentPlayer.currentTime = ratio * self.currentPlayer.duration;
    
    // 4.更新當前播放時間Label,總時長Label的信息和進度條信息,因為updateProgressInfo方法使用self.currentPlayer.currentTime值,所以必須在第3步執行完才能執行updateProgressInfo方法,如果順序反之,則不行
    [self updateProgressInfo];
}

/** 實時更新播放進度信息 */
- (void)updateProgressInfo {
    // 1.更新當前音樂播放時間和當前音樂總時長
    self.currentLabel.text = [NSString stringWithTime:self.currentPlayer.currentTime];
    self.totalLabel.text = [NSString stringWithTime:self.currentPlayer.duration];
    
    // 2.更新進度條信息
    self.progressSlider.value = self.currentPlayer.currentTime / self.currentPlayer.duration;
}

7.歌詞的解析

歌詞模型類功能的實現

  • 歌詞模型類的屬性: 當前行歌詞的播放時間,歌詞內容
  • 實現當前行歌詞的解析,當前行歌詞數據格式: [00:33.20]只是因為在人群中多看了你一眼
#import "WXLrcLineItem.h"

@implementation WXLrcLineItem

/** 創建WXLrcLineItem的對象方法 */
- (instancetype)initWithLrcLineString:(NSString *)lrcLineString
{
    if (self = [super init]) {
        // 1.解析歌詞
        [self lrcStringToItem:lrcLineString];
    }
    return self;
}

/** 創建WXLrcLineItem的類方法 */
+ (instancetype)lrcLineItemWithLrcLineString:(NSString *)lrcLineString
{
    return [[self alloc] initWithLrcLineString:lrcLineString];
}

/** 解析歌詞 */
- (void)lrcStringToItem:(NSString *)lrcLineString
{
    // 歌詞數據: [00:33.20]只是因為在人群中多看了你一眼
    // 1.以"]"切割歌詞與時間
    NSArray *lrcArray = [lrcLineString componentsSeparatedByString:@"]"];
    
    // 2.解析出歌詞內容 只是因為在人群中多看了你一眼
    self.name = lrcArray[1];
    
    // 3.解析時間 [00:31.25
    NSString *timeString = lrcArray[0];
    self.time = [self timeWithTimeString:[timeString substringFromIndex:1]];
}

/** 解析時間 時間數據: 00:31.25 */
- (NSTimeInterval)timeWithTimeString:(NSString *)timeString {
    
    NSInteger min = [[timeString componentsSeparatedByString:@":"][0] integerValue];
    NSInteger sec = [[timeString substringWithRange:NSMakeRange(3, 2)] integerValue];
    NSInteger mSec = [[timeString componentsSeparatedByString:@"."][1] integerValue];
    
    return min * 60 + sec + mSec * 0.01;
}
@end

創建歌詞文件處理WXLrcTool工具類

  • 實現將歌詞文件轉換成歌詞模型數組
/** 傳入本地歌詞文件名,解析歌詞文件 */
+ (NSArray *)lrcToolWithLrcFileName:(NSString *)lrcFileName
{
    // 1.獲取歌詞的路徑
    NSString *filePath = [[NSBundle mainBundle] pathForResource:lrcFileName ofType:nil];
    
    // 2.讀取歌詞文件數據
    NSString *lrcString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    
    // SINGLE: 3.通過\n字符切割到數組
    NSArray *lrcArray = [lrcString componentsSeparatedByString:@"\n"];
    
    /** 
     歌詞文件頭部信息
     [ti:]
     [ar:]
     [al:]
     */
    // 4.遍歷數組,將數組轉模型
    NSMutableArray *lrcArrayM = [NSMutableArray array];
    for (NSString *lrcLineString in lrcArray) {
        // 1.過濾歌詞文件頭部無用信息,不是以[開頭的也過濾掉
        if ([lrcLineString hasPrefix:@"[ti:"] || [lrcLineString hasPrefix:@"[ar:"] || [lrcLineString hasPrefix:@"[al:"] || ![lrcLineString hasPrefix:@"["]) {
            continue;
        }
        
        // 2.解析歌詞數據到模型
        WXLrcLineItem *lrcItem = [WXLrcLineItem lrcLineItemWithLrcLineString:lrcLineString];
        
        // 3.添加到可變數組
        [lrcArrayM addObject:lrcItem];
    }
    
    return lrcArrayM;
}

8.實現歌詞ScrollView歌詞的滾動功能

主界面控制器創建更新歌詞的定時器CADisplayLink定時器

  • 提供創建/移除用于更新歌詞的定時器的方法
  • 在定時器定時執行的方法中將當前的播放時間傳遞給lrcScrollView的currentTime屬性,讓lrcScrollView實現歌詞的進度更新
    self.lrcScrollView.currentTime = self.currentPlayer.currentTime;

自定義顯示歌詞的WXLrcLabel

  • 對外提供當前行歌詞的進度progress屬性,讓外部為其設置當前歌詞的進度
  • 重寫- (void)drawRect:(CGRect)rect方法繪制WXLrcLabel
    • UIRectFill填充Label顏色,不是填充文字顏色. UIRectFill(fullRect);
    • UIRectFillUsingBlendMode填充文字函數,kCGBlendModeSourceOut:表示填充文字以外的區域
- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    
    // 1.設置填充的顏色
    [[UIColor greenColor] set];
    
    // 2.設置要填充的尺寸,根據傳遞過來的歌詞播放進度
    CGRect fullRect = CGRectMake(0, 0, self.bounds.size.width * self.progress, self.bounds.size.height);
    
    // 3.開始繪制
    // SINGLE: UIRectFill會填充Label顏色,不是填充文字顏色
//    UIRectFill(fullRect);
    // SINGLE: UIRectFillUsingBlendMode(fullRect, kCGBlendModeSourceIn)填充文字,kCGBlendModeSourceOut:表示填充文字以外的區域
    UIRectFillUsingBlendMode(fullRect, kCGBlendModeSourceIn);
}
  • 重寫progress當前行歌詞播放的進度,用于刷新重繪Label,在方法內部調用setNeedsDisplay重繪Label

實現歌詞的滾動,當前行歌詞進度顏色填充效果

  • 重寫lrcFileName屬性set方法來設置tableView中的歌詞
    • 歌詞bug注意: 必須將指向當前播放的歌詞行數清0,否則在音樂快播放完成時手動切換下一首會導致程序崩潰.奔潰原因: 假設當前音樂歌詞總60行,下一首音樂歌詞共38行,當前播放到55行是調到下一首,下一首最大才38行,這樣會導致tableView的數據源數組訪問越界.
- (void)setLrcFileName:(NSString *)lrcFileName
{
    // CARE: 0.切換音樂前,將當前播放的歌詞清0,否則會出現當前音樂快播完后,手動切換下一首導致程序奔潰
    // 奔潰原因: 假設當前音樂歌詞總60行,下一首音樂歌詞共38行,當前播放到55行是調到下一首,下一首最大才38行,這樣會導致tableView的數據源數組訪問越界
    // 0.指向當前播放的歌詞行數清0
    self.currentIndex = 0;
    
    // 1.保存歌詞名
    _lrcFileName = lrcFileName;
    
    // 2.解析歌詞,保存到數組
    self.lrcList = [WXLrcTool lrcToolWithLrcFileName:lrcFileName];
    // CARE: 初始設置歌詞的第0行
    WXLrcLineItem *firstItem = self.lrcList[0];
    self.lrcLabel.text = firstItem.name;
    
    // 3.刷新tableView
    [self.tableView reloadData];
}
  • 實時刷新當前歌詞進度
    • 滾動tableView的方法, scrollPosition: UITableViewScrollPositionTop表示tableView滾動到頂部
      *- (void)scrollToRowAtIndexPath:(NSIndexPath )indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;

    • 計算當前行歌詞的進度
      獲取當前行歌詞進度 當前行歌詞進度 = (當前播放的時間 - 當前行歌詞的開始時間) / (下一行歌詞的開始時間 - 當前行歌詞的開始時間)

    • 因該方法每秒執行60次,考慮到內部刷新列表操作和性能問題,判斷如果當前行是正在播放的歌詞,就無需刷新,通過self.currentIndex != i,如果不等于i,才進入刷新列表

    • 設置當前行歌詞后,要刷新當前行和上一行歌詞的cell

/** 重寫當前播放時間set方法,該方法每秒會調用60次,因為外部用CADisplayLink定時器刷新歌詞進度 */
- (void)setCurrentTime:(NSTimeInterval)currentTime
{
    // 1.保存當前播放時間
    _currentTime = currentTime;
    
    // 2.獲取歌詞的總數
    NSInteger count = self.lrcList.count;
    // 3.遍歷歌詞數組
    for (NSInteger i = 0; i < count; i++) {
        // 3.1 獲取第i位置的歌詞模型
        WXLrcLineItem *currentLrcItem = self.lrcList[i];
        
        // 3.2 獲取第i+1位置的歌詞的模型
        NSInteger nextIndex = i + 1;
        WXLrcLineItem *nextLrcItem = nil;
        if (nextIndex < count) {
            nextLrcItem = self.lrcList[nextIndex];
        }
        
        // 3.3 判斷當前播放時間是否在第 i ~ i+1歌詞之間  (i位置的時間 <= self.currentTime < i+1位置的時間)
        // CARE: 因該方法每秒執行60次,考慮到內部刷新列表操作和性能問題,判斷如果當前行是正在播放的歌詞,就無需刷新,通過self.currentIndex != i,如果不等于i,才進入刷新列表
        if ( (self.currentIndex != i) && (currentTime >= currentLrcItem.time && currentTime < nextLrcItem.time) ) {
            
            // 1.滾動當前正在播放的歌詞到中心位置,實際是滾動到最頂部,因為之前有設置內邊距頂部間距是ScrollView的一半
            // SINGLE: 調用哪個tableView的滾動方法
            NSIndexPath *currentIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
            [self.tableView scrollToRowAtIndexPath:currentIndexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
            
            // CARE: 刷新主界面歌詞Label內容
            self.lrcLabel.text = currentLrcItem.name;
            
            // 2.刷新上一行歌詞,如果沒刷新,會導致上一行的歌詞字體樣式和當前歌詞的字體樣式一樣
            NSIndexPath *previousIndexPath = [NSIndexPath indexPathForRow:self.currentIndex inSection:0];
            
            // CARE: 3.記錄當前滾動的歌詞,下面刷新cell有用到self.currentIndex,此處順序不能和以下相反
            self.currentIndex = i;
            
            // 4.刷新當前行和上一行歌詞
            [self.tableView reloadRowsAtIndexPaths:@[currentIndexPath, previousIndexPath] withRowAnimation:UITableViewRowAnimationNone];
            
            // 5.設置重新繪制鎖屏封面和歌詞,鎖屏界面
            [self setupLockImage];
        }
        
        // 4.獲取當前這句歌詞,來獲得當前播放的進度,傳遞當前歌詞進度給cell中lrcLabel
        if (self.currentIndex == i) {
            
            // SINGLE: 1.獲取當前行歌詞進度 當前行歌詞進度 = (當前播放的時間 - 當前行歌詞的開始時間) / (下一行歌詞的開始時間 - 當前行歌詞的開始時間)
            CGFloat progress = (currentTime - currentLrcItem.time) / (nextLrcItem.time - currentLrcItem.time);
            
            // 2.獲取當前顯示歌詞的cell
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
            WXLrcCell *lrcCell = [self.tableView cellForRowAtIndexPath:indexPath];
            
            // 3.設置歌詞進度,傳遞給當前歌詞進度給cell中lrcLabel
            lrcCell.lrcLabel.progress = progress;
            // CARE: 將當前行歌詞進度賦值給主界面傳過來的歌詞Label;
            self.lrcLabel.progress = progress;
        }
    }
}

10.設置主界面的歌詞

在主界面中設置主界面歌詞

  • 在初始化歌詞的ScrollView的setupLrcScrollView方法中初始設置lrcLabel主界面歌詞Label
    self.lrcLabel.text = nil;
    // 3.將主界面的歌詞的Label傳給lrcScrollView的一個屬性 - >lrcLabel,讓lrcScrollView為其文字屬性,歌詞進度賦值
    self.lrcScrollView.lrcLabel = self.lrcLabel;
  • 在scrollViewDidScroll方法中,設置self.lrcLabel的透明度,實現拖動時,主界面歌詞漸變效果
/** 監聽歌詞的lrcScrollView的拖動 */
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    // 1.獲取當前點
    CGPoint curPoint = scrollView.contentOffset;
    
    // 2.通過偏移量獲取滑動的比例
    CGFloat ratio = (1 - curPoint.x / self.lrcScrollView.bounds.size.width);
    
    // 3.改變主界面中間歌手圖片iconView和主界面單行歌詞的透明度
    self.iconView.alpha = ratio;
    self.lrcLabel.alpha = ratio;
}

由lrcScrollView內部為主界面歌詞內容和進度進行更新

  • 在WXLrcScrollView的setCurrentTime方法中設置主界面歌詞的內容和進度
    • 刷新主界面歌詞Label內容
      self.lrcLabel.text = currentLrcItem.name;
    • 將當前行歌詞進度賦值給主界面傳過來的歌詞Label;
      self.lrcLabel.progress = progress;

11.實現鎖屏界面信息展示和操作

鎖屏界面項目配置

  • 設置項目可播放音視頻步驟

    • 配置后臺可播放音視頻 工程文件->Capabilities -> Background modes ->Audio

      7.鎖屏界面項目配置.png

    • 創建后臺播放音視頻的會話,并激活會話

// 1.創建會話
AVAudioSession *session = [AVAudioSession sharedInstance];

// 2.設置類別為后臺播放 AVAudioSessionCategoryPlayback: 類別為后臺播放,該常量字符串在AVAudioSession.h中
[session setCategory:AVAudioSessionCategoryPlayback error:nil];

// 3.激活會話
[session setActive:YES error:nil];

- 在AppDelegate.m的didFinishLaunchingWithOptions方法中**創建后臺播放音視頻的會話,并激活會話**

```objectivec
  // REMARKS: 項目配置后臺可播放音視頻
  // SINGLE: 配置后臺可播放音視頻 工程文件->Capabilities -> Background modes ->Audio
  // CARE: 模擬器上運行時,音樂可后臺運行,但是真機運行默認是不能后臺播放音視頻的,必須在項目中配置以上操作(后臺可播放音視頻),需創建會話,設置會話類別為后臺播放,并激活會話.
  
  // 為確保程序運行時會執行到以下設置音視頻會話(后臺播放會話)代碼,所以放在didFinishLaunchingWithOptions方法中執行
  // 1.創建會話
  AVAudioSession *session = [AVAudioSession sharedInstance];
  
  // 2.設置類別為后臺播放 AVAudioSessionCategoryPlayback: 類別為后臺播放,該常量字符串在AVAudioSession.h中
  [session setCategory:AVAudioSessionCategoryPlayback error:nil];
  
  // 3.激活會話
  [session setActive:YES error:nil];
  
  return YES;
  • 設置鎖屏界面顯示的內容步驟
    • 獲取鎖屏中心
      MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
    • 設置鎖屏中心要展示的信息,通過設置鎖屏中心nowPlayingInfo屬性設置,該屬性是字典
    • 將設置的字典信息賦給nowPlayingInfo屬性
      playingInfoCenter.nowPlayingInfo = playingInfoDict;
    • 讓應用程序開啟遠程事件
      [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
// REMARKS: 設置鎖屏界面
/** 設置鎖屏界面 */
- (void)setupLockScreenInfoWithLockImage:(UIImage *)lockImage
{
    /*
     // 媒體常量
     MPMediaItemPropertyAlbumTitle           // 媒體音樂的標題(或名稱)
     MPMediaItemPropertyAlbumTrackCount
     MPMediaItemPropertyAlbumTrackNumber
     MPMediaItemPropertyArtist               // 作者
     MPMediaItemPropertyArtwork              // 封面
     MPMediaItemPropertyComposer             // 音樂劇作曲家的媒體項目
     MPMediaItemPropertyDiscCount            // 光盤在包含媒體項目的專輯的數目
     MPMediaItemPropertyDiscNumber
     MPMediaItemPropertyGenre
     MPMediaItemPropertyPersistentID
     MPMediaItemPropertyPlaybackDuration     // 媒體項目的播放持續時間(當前播放時間)
     MPMediaItemPropertyTitle                // 顯示在作者和標題上面
     */
    
    // REMARKS: 設置鎖屏界面,MPNowPlayingInfoCenter鎖屏中心類在MediaPlayer框架中,所以需導入MediaPlayer/MediaPlayer.h頭文件
    // 1.獲取當前正在播放的音樂
    WXMusicItem *playingMusicItem = [WXMusicTool playingMusic];
    
    // 2.獲取鎖屏中心
    MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
    
    // 3.設置鎖屏中心要展示的信息,通過設置鎖屏中心nowPlayingInfo屬性設置,該屬性是字典
    // 創建要可變字典,用來存放要顯示在鎖屏中心的信息
    NSMutableDictionary *playingInfoDict = [NSMutableDictionary dictionary];
    // 3.1 設置展示的音樂名稱
    [playingInfoDict setObject:playingMusicItem.name forKey:MPMediaItemPropertyAlbumTitle];
    
    // 3.2 設置展示的歌手名
    [playingInfoDict setObject:playingMusicItem.singer forKey:MPMediaItemPropertyArtist];
    
    // 3.3 設置展示封面
    MPMediaItemArtwork *artWork = [[MPMediaItemArtwork alloc] initWithImage:lockImage];
    [playingInfoDict setObject:artWork forKey:MPMediaItemPropertyArtwork];
    
    // 3.4 設置音樂播放的總時間
    [playingInfoDict setObject:@(self.duration) forKey:MPMediaItemPropertyPlaybackDuration];
    
    // 3.5 設置音樂當前播放的時間
    [playingInfoDict setObject:@(self.currentTime) forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];

    // 3.6 將設置的字典信息賦給nowPlayingInfo屬性
    playingInfoCenter.nowPlayingInfo = playingInfoDict;
    
    // SINGLE: 4.讓應用程序開啟遠程事件
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

12.實現鎖屏鎖屏歌詞展示

繪制鎖屏封面和歌詞

  • 將鎖屏的封面圖片和歌詞重新繪制,生成新圖片,在顯示到顯示歌手圖標的UIImageView上
#pragma mark - 設置鎖屏界面和鎖屏歌詞

/** 繪制鎖屏封面和歌詞 */
- (void)setupLockImage
{
    // 1.獲取當前音樂的模型
    WXMusicItem *currentMusicItem = [WXMusicTool playingMusic];
    
    // 2.從當前音樂模型取出封面圖片
    UIImage *currentImage = [UIImage imageNamed:currentMusicItem.icon];
    
    // 3.獲取當前,上一行,下一行歌詞
    // 3.1 獲取當前行歌詞
    WXLrcLineItem *currentLrcLine = self.lrcList[self.currentIndex];
    
    // 3.2 獲取上一行歌詞
    NSInteger previousIndex = self.currentIndex - 1;
    WXLrcLineItem *previousLrcLine = nil;
    if (previousIndex >= 0) {
        previousLrcLine = self.lrcList[previousIndex];
    }
    
    // 3.3 獲取下一行歌詞
    NSInteger nextIndex = self.currentIndex + 1;
    WXLrcLineItem *nextLrcLine = nil;
    if (nextIndex < self.lrcList.count) {
        nextLrcLine = self.lrcList[nextIndex];
    }
    
    // 4.繪制圖片
    // 4.1 開啟和圖片尺寸一樣的上下文
    UIGraphicsBeginImageContext(currentImage.size);
    
    // 4.2 繪制圖片
    [currentImage drawInRect:CGRectMake(0, 0, currentImage.size.width, currentImage.size.height)];
    
    // 4.3 將歌詞文字繪制上去
    // 設置文字高度
    CGFloat titleH = 32;
    
    // 4.3.1 繪制上一句歌詞和下一句歌詞
    // SINGLE: 設置繪制文字居中
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.alignment = NSTextAlignmentCenter;
    NSDictionary *otherAttr = @{
                                NSFontAttributeName : [UIFont systemFontOfSize:18],
                                NSForegroundColorAttributeName : [UIColor yellowColor],
                                NSParagraphStyleAttributeName : paragraphStyle
                                };
    // 繪制上一句和下一句歌詞(繪制到圖片底部)
    [previousLrcLine.name drawInRect:CGRectMake(0, currentImage.size.height - titleH * 3, currentImage.size.width, titleH) withAttributes:otherAttr];
    [nextLrcLine.name drawInRect:CGRectMake(0, currentImage.size.height - titleH, currentImage.size.width, titleH) withAttributes:otherAttr];
    
    // 4.3.2 繪制當前行歌詞文字
    NSDictionary *currentAttr = @{
                                  NSFontAttributeName : [UIFont systemFontOfSize:24],
                                  NSForegroundColorAttributeName : [UIColor greenColor],
                                  NSParagraphStyleAttributeName : paragraphStyle
                                  };
    [currentLrcLine.name drawInRect:CGRectMake(0, currentImage.size.height - titleH * 2, currentImage.size.width, titleH) withAttributes:currentAttr];
    
    // 4.4 生成繪制好的圖片
    UIImage *lockImage = UIGraphicsGetImageFromCurrentImageContext();
    
    // 4.5 關閉圖形上下文
    UIGraphicsEndImageContext();
    
    // 5.將生成的圖片添加到鎖屏的封面圖片上
    [self setupLockScreenInfoWithLockImage:lockImage];
}

鎖屏界面實現播放,暫停,上一首,下一首功能

  • 監聽遠程事件,實現鎖屏界面可操作播放/暫停,上一首,下一首
#pragma mark - 監聽遠程事件,實現鎖屏界面可操作播放/暫停,上一首,下一首
// REMARKS: 監聽遠程事件,監聽鎖屏界面操作
/** 監聽遠程事件 */
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{
    switch (event.subtype) {
        case UIEventSubtypeRemoteControlPlay:
        case UIEventSubtypeRemoteControlPause:
            [self playOrPause];
            break;
            
        case UIEventSubtypeRemoteControlPreviousTrack:
            [self previousMusic];
            break;
            
        case UIEventSubtypeRemoteControlNextTrack:
            [self nextMusic];
            break;
            
        default:
            break;
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容