模仿QQ音樂播放器歌詞視圖,默認(rèn)進(jìn)入視圖:
當(dāng)手指從右向左滑動時,出現(xiàn)一個滾動歌詞視圖
配圖######
接下來就來模擬普通視圖和滾動歌詞視圖切換
- 視圖層級結(jié)構(gòu)分析:
- 創(chuàng)建一個透明的UIView,覆蓋掉中間的CenterView
- 在這個View中,先添加一個水平方向滾動的ScrollView(命名為HorizontalScrollView)
HorizontalScrollView ContentSize = 2 * ScreenSize
- 在HorizontalScrollView中繼續(xù)添加一個垂直方向滾動的ScrollView(命名為VerticalScrollView)
VerticalScrollView ContentSize = ScreenSize
- 在VerticalScrollView中添加多個Label,每個Label用來顯示一行歌詞
- 判斷Label,設(shè)置當(dāng)前歌詞所在的Label frame,實現(xiàn)放大效果
- 搭建UI 關(guān)鍵代碼
// 設(shè)置歌詞視圖
- (void)setupLyricView{
// 添加控件
[self addSubview:self.horizontalScrollView];
// 設(shè)置約束
[self.horizontalScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
// 占滿視圖
make.edges.mas_equalTo(self);
}];
// 設(shè)置水平滾動ScrollView的ContentSize (垂直方向不希望滾動,所以設(shè)置為0)
self.horizontalScrollView.contentSize = CGSizeMake(SCREEN_SIZE.width * 2, 0);
// 水平滾動ScrollView添加垂直滾動的ScrollView
[self.horizontalScrollView addSubview:self.verticalScrollView];
// 設(shè)置垂直滾動ScrollView的約束
[self.verticalScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self);
make.left.mas_equalTo(self.horizontalScrollView).mas_offset(SCREEN_SIZE.width);
make.size.mas_equalTo(self);
}];
self.horizontalScrollView.backgroundColor = [UIColor greenColor];
self.verticalScrollView.backgroundColor = [UIColor orangeColor];
}
這樣ScrollView的基本視圖就添加完了,暫時先只設(shè)置了水平方向的ScrollView的ContentSize,垂直滾動需要根據(jù)歌詞Label來計算:
細(xì)節(jié)方面:設(shè)置中心的View背景色為透明,關(guān)閉滾動指示條,開啟分頁效果(水平)
- horizontalScrollView滾動時,實現(xiàn)漸隱效果
根據(jù)偏移量,設(shè)置控制器下中心View視圖透明度,因為在自定義View中,需要修改的View視圖在控制器內(nèi),這里使用了Block,也可以使用代理
先聲明一個屬性
@property (nonatomic,copy) void(^scrollBlock)(CGFloat offSetPercent);
設(shè)置水平ScrollView代理,實現(xiàn)代理方法,調(diào)用Block
#pragma mark -- UIScrollViewDelegate
// 滾動水平方向的ScrollView時,根據(jù)滾動設(shè)置控制器下中心View視圖的透明度(實現(xiàn)漸隱效果)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (scrollView == self.horizontalScrollView) {
self.scrollBlock(1-scrollView.contentOffset.x/SCREEN_SIZE.width);
}
}
如果是代理,在控制器下設(shè)置代理對象實現(xiàn)方法即可,這里我使用的是Block,通過回調(diào)的數(shù)據(jù),設(shè)置中心視圖的漸隱效果
// 設(shè)置中心視圖的漸隱效果
__weak typeof(self) weakSelf = self;
[self.centerLyricView setScrollBlock:^(CGFloat percentAlpa) {
NSLog(@"%f",percentAlpa);
weakSelf.verticalCenterView.alpha = percentAlpa;
}];
這樣水平滾動就處理完了,接下來是VerticalScrollView部分的處理
- 聲明一個屬性,用來存放每首歌曲的全部歌詞
// 當(dāng)前歌曲的歌詞模型數(shù)組
@property (nonatomic,strong) NSArray *lyricModelArray;
- 重寫屬性的setter方法
在里面根據(jù)每首歌曲的歌詞數(shù)量創(chuàng)建顯示歌詞的Label
通過數(shù)組長度確定了Label個數(shù),通過Label個數(shù)決定了VerticalScrollView的ContentSize
#pragma mark -- 重寫setter方法
- (void)setLyricModelArray:(NSArray *)lyricModelArray{
_lyricModelArray = lyricModelArray;
// 存放歌詞的Label
for (int i = 0; i < lyricModelArray.count; i ++) {
// 創(chuàng)建歌詞模型
JSLyricModel *model = lyricModelArray[i];
// 創(chuàng)建Label
UILabel *lyricLabel = [[UILabel alloc]init];
lyricLabel.textColor = [UIColor whiteColor];
[self.verticalScrollView addSubview:lyricLabel];
// 設(shè)置約束
[lyricLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.verticalScrollView);
make.height.mas_equalTo(LyricLabelHeight);
// 索引 * 高度
make.top.mas_equalTo(LyricLabelHeight*i);
}];
// 給Label設(shè)置數(shù)據(jù)
lyricLabel.text = model.content;
}
// 設(shè)置垂直滾動ScrollView的ContentSize
self.verticalScrollView.contentSize = CGSizeMake(0, LyricLabelHeight * lyricModelArray.count);
}
- 給滾動歌詞視圖分發(fā)數(shù)據(jù)
因為不需要實時傳遞,只需要在控制器下,獲取到每一首解析后的歌詞數(shù)據(jù)時,一次傳遞即可
設(shè)置數(shù)據(jù)的方法,在每次切換歌曲時都會被調(diào)用,所以在設(shè)置數(shù)據(jù)方法中為滾動歌曲視圖傳遞數(shù)據(jù)
// 給垂?jié)L動視圖傳遞歌詞數(shù)據(jù)
self.centerLyricView.lyricModelArray = self.lyricModelArray;
這樣基本視圖搭建完成:
- 細(xì)節(jié)處理 設(shè)置內(nèi)邊距&偏移
給VerticalScrollView設(shè)置內(nèi)邊距和偏移量,讓歌詞劃出時,第一句歌詞默認(rèn)居中顯示
需要注意的是要在layoutSubviews來設(shè)置,這里才能拿到當(dāng)前view的真實Frame,如果在屬性的setter方法中,拿到的不是有效數(shù)據(jù),默認(rèn)按照4s的屏幕尺寸計算(最小屏幕計算)
- (void)layoutSubviews{
[super layoutSubviews];
// 設(shè)置外邊距
self.verticalScrollView.contentInset = UIEdgeInsetsMake((self.bounds.size.height-LyricLabelHeight) * 0.5, 0, 0, 0);
self.verticalScrollView.contentOffset = CGPointMake(0, -(self.bounds.size.height-LyricLabelHeight) * 0.5);
}
- 歌詞跟隨滾動,當(dāng)前歌詞字體放大
因為控制器下已經(jīng)計算過當(dāng)前歌詞索引,所以直接聲明屬性傳遞即可
// 當(dāng)前歌詞索引
@property (nonatomic,assign) NSInteger currentLyricIndex;
控制器下傳遞數(shù)據(jù)(在更新歌詞方法中獲取到索引):
self.centerLyricView.currentLyricIndex = self.currentLyricIndex;
重寫歌詞索引的setter方法:
1.根據(jù)索引設(shè)置滾動效果
這里需要使用到LayoutSubViews中設(shè)置VerticalScrollView的內(nèi)邊距/偏移量,所以抽取一個宏
#define VERTICAL_SCROLLVIEW_OFFSET ((self.bounds.size.height-LyricLabelHeight) * 0.5)
// 設(shè)置滾動 (根據(jù)索引設(shè)置偏移量實現(xiàn)滾動: 偏移量 = 索引 * Label高度 )
self.verticalScrollView.contentOffset = CGPointMake(0, currentLyricIndex * LyricLabelHeight);
關(guān)鍵點:
需要注意切換歌曲時,通過VerticalScrollView的subViews可以獲取到歌詞Label,清除之前的Label子視圖,否則歌詞的Label會疊加顯示(歌詞模型數(shù)組setter方法中):
// 每次切歌先移除子視圖 makeObjectsPerformSelector讓所有對象都會去執(zhí)行某一個方法
[self.verticalScrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
2.當(dāng)前歌詞字號放大
設(shè)置當(dāng)前Label的font大于正常的Label
currentLabel.font = [UIFont systemFontOfSize:21]; // 放大字體
當(dāng)切換下一句歌詞時,恢復(fù)上一句歌詞的Label
// 將之前索引對應(yīng)的歌詞字體大小恢復(fù)
UILabel *previousLyricLabel = self.verticalScrollView.subviews[_currentLyricIndex];
previousLyricLabel.font = [UIFont systemFontOfSize:17];// 字體默認(rèn)大小17
關(guān)鍵點:
假如上一句歌詞索引是0,拖拽到索引20時,這時上一句歌詞的索引并不是當(dāng)前索引-1,所以直接利用了帶下劃線的屬性,來獲取上一句歌詞對應(yīng)的Label
/*
在_currentLyricIndex = currentLyricIndex;
賦值前 _currentLyricIndex --> 上一句歌詞的索引
*/
_currentLyricIndex = currentLyricIndex;
3.當(dāng)前歌詞變色
上一篇文章中實現(xiàn)歌詞變色自定義了一個Label,只需要將創(chuàng)建的Label改為自定義Label類型,當(dāng)前View下,已經(jīng)有了當(dāng)前歌詞索引,當(dāng)前歌詞數(shù)組,在上一篇設(shè)置歌詞變色中已經(jīng)在控制器下計算了變色的進(jìn)度,所以只需要一個變色的進(jìn)度就可以了
聲明屬性:
// 當(dāng)前歌詞的進(jìn)度
@property (nonatomic,assign) CGFloat currentLyricProgress;
控制器下傳遞進(jìn)度數(shù)據(jù)
self.centerLyricView.currentLyricProgress = averageProgress;
重寫currentLyricProgress 屬性setter方法中,獲取到當(dāng)前歌詞Label,給自定義Label的進(jìn)度屬性賦值
// 當(dāng)前歌詞進(jìn)度setter方法
- (void)setCurrentLyricProgress:(CGFloat)currentLyricProgress{
_currentLyricProgress = currentLyricProgress;
// 設(shè)置當(dāng)前Label進(jìn)度
JSColorLabel *currentLabel = self.verticalScrollView.subviews[self.currentLyricIndex];
currentLabel.progress = currentLyricProgress;
}
與設(shè)置Label字號一樣,還需要恢復(fù)上一句歌詞的顏色,在重寫當(dāng)前索引屬性方法中通過_currentLyricIndex拿到上一句歌詞
// 恢復(fù)上一句歌詞的顏色
JSColorLabel *previousLabel = self.verticalScrollView.subviews[_currentLyricIndex];
previousLabel.progress = 0;
最后,防止切歌的時候索引越界,在重寫索引屬性的setter方法中,恢復(fù)上一句歌詞狀態(tài)時,需要進(jìn)行判斷
// 切歌索引處理,防止索引越界
if (currentLyricIndex != 0) { // 索引=0 代表在切歌
// 將之前索引對應(yīng)的歌詞字體大小和顏色恢復(fù)
JSColorLabel *previousLyricLabel = self.verticalScrollView.subviews[_currentLyricIndex];
previousLyricLabel.progress = 0; // 恢復(fù)上一句歌詞的顏色
previousLyricLabel.font = [UIFont systemFontOfSize:17]; // 恢復(fù)上一句歌詞的字體默認(rèn)大小
}
到此,滾動歌詞視圖設(shè)置完畢
完整代碼:
.h
#import <UIKit/UIKit.h>
@interface JSCenterLyricView : UIView
// 滾動時偏移量占屏幕的比例
@property (nonatomic,copy) void(^scrollBlock)(CGFloat offSetPercent);
// 當(dāng)前歌曲的歌詞模型數(shù)組
@property (nonatomic,strong) NSArray *lyricModelArray;
// 當(dāng)前歌詞索引
@property (nonatomic,assign) NSInteger currentLyricIndex;
// 當(dāng)前歌詞的進(jìn)度
@property (nonatomic,assign) CGFloat currentLyricProgress;
@end
.m
#import "JSCenterLyricView.h"
#import "JSLyricModel.h"
#import "JSColorLabel.h"
#import "Masonry.h"
#define SCREEN_SIZE ([UIScreen mainScreen].bounds.size)
#define VERTICAL_SCROLLVIEW_OFFSET ((self.bounds.size.height-LyricLabelHeight) * 0.5)
// 靜態(tài)全局變量 存放Label的高度 宏處于預(yù)編譯階段,會延長編譯時間
static CGFloat const LyricLabelHeight = 40;
@interface JSCenterLyricView () <UIScrollViewDelegate>
// 水平滾動ScrollView
@property (nonatomic,strong) UIScrollView *horizontalScrollView;
// 垂直滾動ScrollView
@property (nonatomic,strong) UIScrollView *verticalScrollView;
@end
@implementation JSCenterLyricView
/*
initWithCoder : 從文件創(chuàng)建時調(diào)用,相當(dāng)于初始化
awakeFromNib也可以,相當(dāng)于ViewDidLoad,initWithCoder調(diào)用順序先于awakeFromNib
*/
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setupLyricView];
}
return self;
}
// 設(shè)置歌詞視圖
- (void)setupLyricView{
// 添加控件
[self addSubview:self.horizontalScrollView];
// 設(shè)置約束
[self.horizontalScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
// 占滿視圖
make.edges.mas_equalTo(self);
}];
// 設(shè)置水平滾動ScrollView的ContentSize (垂直方向不希望滾動,所以設(shè)置為0)
self.horizontalScrollView.contentSize = CGSizeMake(SCREEN_SIZE.width * 2, 0);
// 水平滾動ScrollView添加垂直滾動的ScrollView
[self.horizontalScrollView addSubview:self.verticalScrollView];
// 設(shè)置垂直滾動ScrollView的約束
[self.verticalScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self);
make.left.mas_equalTo(self.horizontalScrollView).mas_offset(SCREEN_SIZE.width);
make.size.mas_equalTo(self.horizontalScrollView);
}];
// 關(guān)閉滾動指示條 (水平滾動開啟分頁)
self.horizontalScrollView.pagingEnabled = YES;
self.horizontalScrollView.bounces = NO;
self.horizontalScrollView.showsVerticalScrollIndicator = NO;
self.horizontalScrollView.showsHorizontalScrollIndicator = NO;
self.verticalScrollView.showsHorizontalScrollIndicator = NO;
self.verticalScrollView.showsVerticalScrollIndicator = NO;
}
#pragma mark -- 重寫setter方法
// 歌詞模型數(shù)組setter方法
- (void)setLyricModelArray:(NSArray *)lyricModelArray{
// 每次切歌先移除子視圖 makeObjectsPerformSelector讓所有對象都會去執(zhí)行某一個方法
[self.verticalScrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
_lyricModelArray = lyricModelArray;
// 存放歌詞的Label
for (int i = 0; i < lyricModelArray.count; i ++) {
// 創(chuàng)建歌詞模型
JSLyricModel *model = lyricModelArray[i];
// 創(chuàng)建Label
JSColorLabel *lyricLabel = [[JSColorLabel alloc]init];
lyricLabel.textColor = [UIColor whiteColor];
[self.verticalScrollView addSubview:lyricLabel];
// 設(shè)置約束
[lyricLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.verticalScrollView);
make.height.mas_equalTo(LyricLabelHeight);
// 索引 * 高度
make.top.mas_equalTo(LyricLabelHeight*i);
}];
// 給Label設(shè)置數(shù)據(jù)
lyricLabel.text = model.content;
}
// 設(shè)置垂直滾動ScrollView的ContentSize
self.verticalScrollView.contentSize = CGSizeMake(0, LyricLabelHeight * lyricModelArray.count);
}
// 歌詞索引setter方法
- (void)setCurrentLyricIndex:(NSInteger)currentLyricIndex{
// 切歌索引處理,防止索引越界
if (currentLyricIndex != 0) { // 索引=0 代表切換歌曲
// 將之前索引對應(yīng)的歌詞字體大小和顏色恢復(fù)
JSColorLabel *previousLyricLabel = self.verticalScrollView.subviews[_currentLyricIndex];
previousLyricLabel.progress = 0; // 恢復(fù)上一句歌詞的顏色
previousLyricLabel.font = [UIFont systemFontOfSize:17]; // 恢復(fù)上一句歌詞的字體默認(rèn)大小
}
/*
在_currentLyricIndex = currentLyricIndex;
賦值前 _currentLyricIndex --> 上一句歌詞的索引
*/
_currentLyricIndex = currentLyricIndex;
// 設(shè)置滾動 (根據(jù)索引設(shè)置偏移量實現(xiàn)滾動: 偏移量 = 默認(rèn)偏移量 + 索引 * Label高度 )
[self.verticalScrollView setContentOffset:CGPointMake(0, -VERTICAL_SCROLLVIEW_OFFSET + currentLyricIndex * LyricLabelHeight) animated:YES];
// 設(shè)置當(dāng)前Label字號放大 (根據(jù)索引取出Label)
JSColorLabel *currentLabel = self.verticalScrollView.subviews[currentLyricIndex];
// 設(shè)置當(dāng)前Label字體大小
currentLabel.font = [UIFont systemFontOfSize:21]; // 放大字體
}
// 當(dāng)前歌詞進(jìn)度setter方法
- (void)setCurrentLyricProgress:(CGFloat)currentLyricProgress{
_currentLyricProgress = currentLyricProgress;
// 設(shè)置當(dāng)前Label進(jìn)度
JSColorLabel *currentLabel = self.verticalScrollView.subviews[self.currentLyricIndex];
currentLabel.progress = currentLyricProgress;
}
- (void)layoutSubviews{
[super layoutSubviews];
// 設(shè)置內(nèi)邊距
self.verticalScrollView.contentInset = UIEdgeInsetsMake(VERTICAL_SCROLLVIEW_OFFSET, 0, VERTICAL_SCROLLVIEW_OFFSET, 0);
// 設(shè)置默認(rèn)的偏移量
self.verticalScrollView.contentOffset = CGPointMake(0, -VERTICAL_SCROLLVIEW_OFFSET);
}
#pragma mark -- 懶加載
- (UIScrollView *)horizontalScrollView{
if (_horizontalScrollView == nil) {
_horizontalScrollView = [[UIScrollView alloc]init];
_horizontalScrollView.delegate = self;
}
return _horizontalScrollView;
}
- (UIScrollView *)verticalScrollView{
if (_verticalScrollView == nil) {
_verticalScrollView = [[UIScrollView alloc]init];
}
return _verticalScrollView;
}
#pragma mark -- UIScrollViewDelegate
// 滾動水平方向的ScrollView時,根據(jù)滾動設(shè)置控制器下中心View視圖的透明度(實現(xiàn)漸隱效果)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (scrollView == self.horizontalScrollView) {
self.scrollBlock(1-scrollView.contentOffset.x/SCREEN_SIZE.width);
}
}
@end