歌词处理-滚动歌词视图 - (Obj-C)

模仿QQ音乐播放器歌词视图,默认进入视图:

歌词处理-滚动歌词视图 - (Obj-C)_第1张图片
歌词视图_1.png

当手指从右向左滑动时,出现一个滚动歌词视图

配图######

接下来就来模拟普通视图和滚动歌词视图切换

  • 视图层级结构分析:
歌词处理-滚动歌词视图 - (Obj-C)_第2张图片
滚动歌词层级结构.png
歌词处理-滚动歌词视图 - (Obj-C)_第3张图片
ScrollView视图结构.png
  1. 创建一个透明的UIView,覆盖掉中间的CenterView
  2. 在这个View中,先添加一个水平方向滚动的ScrollView(命名为HorizontalScrollView)
  HorizontalScrollView  ContentSize =  2 * ScreenSize
  1. 在HorizontalScrollView中继续添加一个垂直方向滚动的ScrollView(命名为VerticalScrollView)
 VerticalScrollView  ContentSize =  ScreenSize
  1. 在VerticalScrollView中添加多个Label,每个Label用来显示一行歌词
  2. 判断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来计算:

歌词处理-滚动歌词视图 - (Obj-C)_第4张图片
歌词视图_2.png

细节方面:设置中心的View背景色为透明,关闭滚动指示条,开启分页效果(水平)

歌词处理-滚动歌词视图 - (Obj-C)_第5张图片
歌词视图_3.png
  • 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;

这样基本视图搭建完成:

歌词处理-滚动歌词视图 - (Obj-C)_第6张图片
歌词视图_4.png
  • 细节处理 设置内边距&偏移

给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;
歌词处理-滚动歌词视图 - (Obj-C)_第7张图片
歌词视图_5.png

最后,防止切歌的时候索引越界,在重写索引属性的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

你可能感兴趣的:(歌词处理-滚动歌词视图 - (Obj-C))