模仿QQ音乐播放器歌词视图,默认进入视图:
当手指从右向左滑动时,出现一个滚动歌词视图
配图######
接下来就来模拟普通视图和滚动歌词视图切换
- 视图层级结构分析:
- 创建一个透明的UIView,覆盖掉中间的CenterView
- 在这个View中,先添加一个水平方向滚动的ScrollView(命名为HorizontalScrollView)
HorizontalScrollView ContentSize = 2 * ScreenSize
- 在HorizontalScrollView中继续添加一个垂直方向滚动的ScrollView(命名为VerticalScrollView)
VerticalScrollView ContentSize = ScreenSize
- 在VerticalScrollView中添加多个Label,每个Label用来显示一行歌词
- 判断Label,设置当前歌词所在的Label frame,实现放大效果
- 搭建UI 关键代码
// 设置歌词视图
- (void)setupLyricView{
// 添加控件
[self addSubview:self.horizontalScrollView];
// 设置约束
[self.horizontalScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
// 占满视图
make.edges.mas_equalTo(self);
}];
// 设置水平滚动ScrollView的ContentSize (垂直方向不希望滚动,所以设置为0)
self.horizontalScrollView.contentSize = CGSizeMake(SCREEN_SIZE.width * 2, 0);
// 水平滚动ScrollView添加垂直滚动的ScrollView
[self.horizontalScrollView addSubview:self.verticalScrollView];
// 设置垂直滚动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的基本视图就添加完了,暂时先只设置了水平方向的ScrollView的ContentSize,垂直滚动需要根据歌词Label来计算:
细节方面:设置中心的View背景色为透明,关闭滚动指示条,开启分页效果(水平)
- horizontalScrollView滚动时,实现渐隐效果
根据偏移量,设置控制器下中心View视图透明度,因为在自定义View中,需要修改的View视图在控制器内,这里使用了Block,也可以使用代理
先声明一个属性
@property (nonatomic,copy) void(^scrollBlock)(CGFloat offSetPercent);
设置水平ScrollView代理,实现代理方法,调用Block
#pragma mark -- UIScrollViewDelegate
// 滚动水平方向的ScrollView时,根据滚动设置控制器下中心View视图的透明度(实现渐隐效果)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (scrollView == self.horizontalScrollView) {
self.scrollBlock(1-scrollView.contentOffset.x/SCREEN_SIZE.width);
}
}
如果是代理,在控制器下设置代理对象实现方法即可,这里我使用的是Block,通过回调的数据,设置中心视图的渐隐效果
// 设置中心视图的渐隐效果
__weak typeof(self) weakSelf = self;
[self.centerLyricView setScrollBlock:^(CGFloat percentAlpa) {
NSLog(@"%f",percentAlpa);
weakSelf.verticalCenterView.alpha = percentAlpa;
}];
这样水平滚动就处理完了,接下来是VerticalScrollView部分的处理
- 声明一个属性,用来存放每首歌曲的全部歌词
// 当前歌曲的歌词模型数组
@property (nonatomic,strong) NSArray *lyricModelArray;
- 重写属性的setter方法
在里面根据每首歌曲的歌词数量创建显示歌词的Label
通过数组长度确定了Label个数,通过Label个数决定了VerticalScrollView的ContentSize
#pragma mark -- 重写setter方法
- (void)setLyricModelArray:(NSArray *)lyricModelArray{
_lyricModelArray = lyricModelArray;
// 存放歌词的Label
for (int i = 0; i < lyricModelArray.count; i ++) {
// 创建歌词模型
JSLyricModel *model = lyricModelArray[i];
// 创建Label
UILabel *lyricLabel = [[UILabel alloc]init];
lyricLabel.textColor = [UIColor whiteColor];
[self.verticalScrollView addSubview:lyricLabel];
// 设置约束
[lyricLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.verticalScrollView);
make.height.mas_equalTo(LyricLabelHeight);
// 索引 * 高度
make.top.mas_equalTo(LyricLabelHeight*i);
}];
// 给Label设置数据
lyricLabel.text = model.content;
}
// 设置垂直滚动ScrollView的ContentSize
self.verticalScrollView.contentSize = CGSizeMake(0, LyricLabelHeight * lyricModelArray.count);
}
- 给滚动歌词视图分发数据
因为不需要实时传递,只需要在控制器下,获取到每一首解析后的歌词数据时,一次传递即可
设置数据的方法,在每次切换歌曲时都会被调用,所以在设置数据方法中为滚动歌曲视图传递数据
// 给垂滚动视图传递歌词数据
self.centerLyricView.lyricModelArray = self.lyricModelArray;
这样基本视图搭建完成:
- 细节处理 设置内边距&偏移
给VerticalScrollView设置内边距和偏移量,让歌词划出时,第一句歌词默认居中显示
需要注意的是要在layoutSubviews来设置,这里才能拿到当前view的真实Frame,如果在属性的setter方法中,拿到的不是有效数据,默认按照4s的屏幕尺寸计算(最小屏幕计算)
- (void)layoutSubviews{
[super layoutSubviews];
// 设置外边距
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);
}
- 歌词跟随滚动,当前歌词字体放大
因为控制器下已经计算过当前歌词索引,所以直接声明属性传递即可
// 当前歌词索引
@property (nonatomic,assign) NSInteger currentLyricIndex;
控制器下传递数据(在更新歌词方法中获取到索引):
self.centerLyricView.currentLyricIndex = self.currentLyricIndex;
重写歌词索引的setter方法:
1.根据索引设置滚动效果
这里需要使用到LayoutSubViews中设置VerticalScrollView的内边距/偏移量,所以抽取一个宏
#define VERTICAL_SCROLLVIEW_OFFSET ((self.bounds.size.height-LyricLabelHeight) * 0.5)
// 设置滚动 (根据索引设置偏移量实现滚动: 偏移量 = 索引 * Label高度 )
self.verticalScrollView.contentOffset = CGPointMake(0, currentLyricIndex * LyricLabelHeight);
关键点:
需要注意切换歌曲时,通过VerticalScrollView的subViews可以获取到歌词Label,清除之前的Label子视图,否则歌词的Label会叠加显示(歌词模型数组setter方法中):
// 每次切歌先移除子视图 makeObjectsPerformSelector让所有对象都会去执行某一个方法
[self.verticalScrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
2.当前歌词字号放大
设置当前Label的font大于正常的Label
currentLabel.font = [UIFont systemFontOfSize:21]; // 放大字体
当切换下一句歌词时,恢复上一句歌词的Label
// 将之前索引对应的歌词字体大小恢复
UILabel *previousLyricLabel = self.verticalScrollView.subviews[_currentLyricIndex];
previousLyricLabel.font = [UIFont systemFontOfSize:17];// 字体默认大小17
关键点:
假如上一句歌词索引是0,拖拽到索引20时,这时上一句歌词的索引并不是当前索引-1,所以直接利用了带下划线的属性,来获取上一句歌词对应的Label
/*
在_currentLyricIndex = currentLyricIndex;
赋值前 _currentLyricIndex --> 上一句歌词的索引
*/
_currentLyricIndex = currentLyricIndex;
3.当前歌词变色
上一篇文章中实现歌词变色自定义了一个Label,只需要将创建的Label改为自定义Label类型,当前View下,已经有了当前歌词索引,当前歌词数组,在上一篇设置歌词变色中已经在控制器下计算了变色的进度,所以只需要一个变色的进度就可以了
声明属性:
// 当前歌词的进度
@property (nonatomic,assign) CGFloat currentLyricProgress;
控制器下传递进度数据
self.centerLyricView.currentLyricProgress = averageProgress;
重写currentLyricProgress 属性setter方法中,获取到当前歌词Label,给自定义Label的进度属性赋值
// 当前歌词进度setter方法
- (void)setCurrentLyricProgress:(CGFloat)currentLyricProgress{
_currentLyricProgress = currentLyricProgress;
// 设置当前Label进度
JSColorLabel *currentLabel = self.verticalScrollView.subviews[self.currentLyricIndex];
currentLabel.progress = currentLyricProgress;
}
与设置Label字号一样,还需要恢复上一句歌词的颜色,在重写当前索引属性方法中通过_currentLyricIndex拿到上一句歌词
// 恢复上一句歌词的颜色
JSColorLabel *previousLabel = self.verticalScrollView.subviews[_currentLyricIndex];
previousLabel.progress = 0;
最后,防止切歌的时候索引越界,在重写索引属性的setter方法中,恢复上一句歌词状态时,需要进行判断
// 切歌索引处理,防止索引越界
if (currentLyricIndex != 0) { // 索引=0 代表在切歌
// 将之前索引对应的歌词字体大小和颜色恢复
JSColorLabel *previousLyricLabel = self.verticalScrollView.subviews[_currentLyricIndex];
previousLyricLabel.progress = 0; // 恢复上一句歌词的颜色
previousLyricLabel.font = [UIFont systemFontOfSize:17]; // 恢复上一句歌词的字体默认大小
}
到此,滚动歌词视图设置完毕
完整代码:
.h
#import
@interface JSCenterLyricView : UIView
// 滚动时偏移量占屏幕的比例
@property (nonatomic,copy) void(^scrollBlock)(CGFloat offSetPercent);
// 当前歌曲的歌词模型数组
@property (nonatomic,strong) NSArray *lyricModelArray;
// 当前歌词索引
@property (nonatomic,assign) NSInteger currentLyricIndex;
// 当前歌词的进度
@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)
// 静态全局变量 存放Label的高度 宏处于预编译阶段,会延长编译时间
static CGFloat const LyricLabelHeight = 40;
@interface JSCenterLyricView ()
// 水平滚动ScrollView
@property (nonatomic,strong) UIScrollView *horizontalScrollView;
// 垂直滚动ScrollView
@property (nonatomic,strong) UIScrollView *verticalScrollView;
@end
@implementation JSCenterLyricView
/*
initWithCoder : 从文件创建时调用,相当于初始化
awakeFromNib也可以,相当于ViewDidLoad,initWithCoder调用顺序先于awakeFromNib
*/
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setupLyricView];
}
return self;
}
// 设置歌词视图
- (void)setupLyricView{
// 添加控件
[self addSubview:self.horizontalScrollView];
// 设置约束
[self.horizontalScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
// 占满视图
make.edges.mas_equalTo(self);
}];
// 设置水平滚动ScrollView的ContentSize (垂直方向不希望滚动,所以设置为0)
self.horizontalScrollView.contentSize = CGSizeMake(SCREEN_SIZE.width * 2, 0);
// 水平滚动ScrollView添加垂直滚动的ScrollView
[self.horizontalScrollView addSubview:self.verticalScrollView];
// 设置垂直滚动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);
}];
// 关闭滚动指示条 (水平滚动开启分页)
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方法
// 歌词模型数组setter方法
- (void)setLyricModelArray:(NSArray *)lyricModelArray{
// 每次切歌先移除子视图 makeObjectsPerformSelector让所有对象都会去执行某一个方法
[self.verticalScrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
_lyricModelArray = lyricModelArray;
// 存放歌词的Label
for (int i = 0; i < lyricModelArray.count; i ++) {
// 创建歌词模型
JSLyricModel *model = lyricModelArray[i];
// 创建Label
JSColorLabel *lyricLabel = [[JSColorLabel alloc]init];
lyricLabel.textColor = [UIColor whiteColor];
[self.verticalScrollView addSubview:lyricLabel];
// 设置约束
[lyricLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.verticalScrollView);
make.height.mas_equalTo(LyricLabelHeight);
// 索引 * 高度
make.top.mas_equalTo(LyricLabelHeight*i);
}];
// 给Label设置数据
lyricLabel.text = model.content;
}
// 设置垂直滚动ScrollView的ContentSize
self.verticalScrollView.contentSize = CGSizeMake(0, LyricLabelHeight * lyricModelArray.count);
}
// 歌词索引setter方法
- (void)setCurrentLyricIndex:(NSInteger)currentLyricIndex{
// 切歌索引处理,防止索引越界
if (currentLyricIndex != 0) { // 索引=0 代表切换歌曲
// 将之前索引对应的歌词字体大小和颜色恢复
JSColorLabel *previousLyricLabel = self.verticalScrollView.subviews[_currentLyricIndex];
previousLyricLabel.progress = 0; // 恢复上一句歌词的颜色
previousLyricLabel.font = [UIFont systemFontOfSize:17]; // 恢复上一句歌词的字体默认大小
}
/*
在_currentLyricIndex = currentLyricIndex;
赋值前 _currentLyricIndex --> 上一句歌词的索引
*/
_currentLyricIndex = currentLyricIndex;
// 设置滚动 (根据索引设置偏移量实现滚动: 偏移量 = 默认偏移量 + 索引 * Label高度 )
[self.verticalScrollView setContentOffset:CGPointMake(0, -VERTICAL_SCROLLVIEW_OFFSET + currentLyricIndex * LyricLabelHeight) animated:YES];
// 设置当前Label字号放大 (根据索引取出Label)
JSColorLabel *currentLabel = self.verticalScrollView.subviews[currentLyricIndex];
// 设置当前Label字体大小
currentLabel.font = [UIFont systemFontOfSize:21]; // 放大字体
}
// 当前歌词进度setter方法
- (void)setCurrentLyricProgress:(CGFloat)currentLyricProgress{
_currentLyricProgress = currentLyricProgress;
// 设置当前Label进度
JSColorLabel *currentLabel = self.verticalScrollView.subviews[self.currentLyricIndex];
currentLabel.progress = currentLyricProgress;
}
- (void)layoutSubviews{
[super layoutSubviews];
// 设置内边距
self.verticalScrollView.contentInset = UIEdgeInsetsMake(VERTICAL_SCROLLVIEW_OFFSET, 0, VERTICAL_SCROLLVIEW_OFFSET, 0);
// 设置默认的偏移量
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时,根据滚动设置控制器下中心View视图的透明度(实现渐隐效果)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (scrollView == self.horizontalScrollView) {
self.scrollBlock(1-scrollView.contentOffset.x/SCREEN_SIZE.width);
}
}
@end