这里自定义了一个Label,通过DrawRect方法获取Label的图形上下文,使用混合填充的方式实现Label绘制颜色
- 先介绍一下混合填充的参数:
void UIRectFillUsingBlendMode(CGRect rect, CGBlendMode blendMode);
CGBlendMode参数为一个枚举类型:
/* 对应公式(其余是固定的):
result, source, and destination colors with alpha;
Ra, Sa, and Da are the alpha components of these colors.
R --> result
S --> source
D --> destination
kCGBlendModeNormal, R = S + D*(1 - Sa)
kCGBlendModeMultiply,
kCGBlendModeScreen,
kCGBlendModeOverlay,
kCGBlendModeDarken,
kCGBlendModeLighten,
kCGBlendModeColorDodge,
kCGBlendModeColorBurn,
kCGBlendModeSoftLight,
kCGBlendModeHardLight,
kCGBlendModeDifference,
kCGBlendModeExclusion,
kCGBlendModeHue,
kCGBlendModeSaturation,
kCGBlendModeColor,
kCGBlendModeLuminosity,
kCGBlendModeClear, R = 0
kCGBlendModeCopy, R = S
kCGBlendModeSourceIn, R = S*Da
kCGBlendModeSourceOut, R = S*(1 - Da)
kCGBlendModeSourceAtop, R = S*Da + D*(1 - Sa)
kCGBlendModeDestinationOver, R = S*(1 - Da) + D
kCGBlendModeDestinationIn, R = D*Sa
kCGBlendModeDestinationOut, R = D*(1 - Sa)
kCGBlendModeDestinationAtop, R = S*(1 - Da) + D*Sa
kCGBlendModeXOR, R = S*(1 - Da) + D*(1 - Sa)
kCGBlendModePlusDarker, R = MAX(0, (1 - D) + (1 - S))
kCGBlendModePlusLighter R = MIN(1, S + D)
*/
kCGBlendModeNormal样式公式为:
R = S + D * ( 1 - Sa )
结果 = 源颜色 + 目标颜色 * (1-源颜色各透明组件的透明度)
以kCGBlendModeNormal为例,在这里,我们填充的是一个颜色,颜色的透明度为1,也就是源颜色透明度为1,所以Sa = 1
R = S + D*(1 - Sa) --> R = S + D*(1 - 1) --> R = S
这种情况下, kCGBlendModeNormal 和kCGBlendModeCopy类型是一样的效果(使用的就是源颜色填充)
kCGBlendModeCopy, R = S
- 实现歌词变色我们需要使用到的是kCGBlendModeSourceIn:
kCGBlendModeSourceIn, R = S*Da -> 结果 = 源颜色*目标透明度
我们这个案例中的源颜色和目标颜色:
源颜色 --> 就是要绘制上去的颜色/填充色 ([[UIColor greenColor] setFill];)
目标颜色 --> Label当前的颜色(文字颜色和透明),上下文中已经有的颜色
D: Label
默认的文字部分有有颜色 透明度是1
其余部分使用的是透明色 透明度是0
S: 填充色(源颜色)
当前图形上下文中的内容的不透明度
结合公式: R = S*Da ,当混合填充时
文字部分: R = S * Da (Da=1) -> R = S -> 显示的就是源颜色(填充色)
其余部分: R = S * Da (Da=0) -> R = 0 -> 不进行填充/显示目标颜色原有的颜色(透明色)
- 声明属性,存放当前变色歌词进度,在setter方法中执行重绘
// 更新进度的时候执行重绘
- (void)setProgress:(CGFloat)progress{
_progress = progress;
// 执行重绘
[self setNeedsDisplay];
}
- 设置歌词变色的进度
lrc格式的歌词文件无法实现根据节奏设置变色进度,这里取平均值:
每一句歌词在每句歌词显示的总时间内,匀速的变色
平均速度进行计算 : (当前播放时间 - 当前句起始时间) / 当前句总时间
当前句总时间: (下一句的起始时间 - 当前句的起始时间)
因为歌词变色进度也是需要实时更新的,所以也是需要在控制器下的定时器方法内执行的,这里就用到了当当前歌词索引为最后一条时,自定义的一条虚拟歌词对象
CGFloat averageProgress = ([JSMusciManager sharedMusicManager].currentTime - currentLyric.initialTime) / (nextLyric.initialTime - currentLyric.initialTime);
接下来就是导入自定义Label头文件,身份检测器下绑定,修改Label类型,传递数据,这样就可以实现歌词变色了
自定义Label代码:
#import "JSColorLabel.h"
@implementation JSColorLabel
- (void)drawRect:(CGRect)rect {
// 调用父类方法: 将Label上的文字绘制上
[super drawRect:rect];
// 设置填充色
// [[UIColor greenColor] setStroke]; // 描边
[[UIColor greenColor] setFill]; // 填充
// 设置填充色的区域 (默认文字为白色,填充后为绿色,只需要根据当前歌词显示进度来改变填充的宽度,其他不变)
rect = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width *self.progress, rect.size.height);
// 渲染
// 在某个区域中使用混合模式进行填充
/*
kCGBlendModeNormal公式: R = S + D*(1 - Sa) --> 结果 = 源颜色 + 目标颜色 * (1-源颜色各透明组件的透明度)
在这里;
源颜色 --> 就是要绘制上去的颜色/填充色 ([[UIColor greenColor] setFill];)
目标颜色 --> Label当前的颜色(白色和透明),上下文中已经有的颜色
*/
UIRectFillUsingBlendMode(rect, kCGBlendModeSourceIn);
/* 对应公式(其余是固定的):
result, source, and destination colors with alpha;
Ra, Sa, and Da are the alpha components of these colors.
R --> result
S --> source
D --> destination
kCGBlendModeNormal, R = S + D*(1 - Sa)
kCGBlendModeMultiply,
kCGBlendModeScreen,
kCGBlendModeOverlay,
kCGBlendModeDarken,
kCGBlendModeLighten,
kCGBlendModeColorDodge,
kCGBlendModeColorBurn,
kCGBlendModeSoftLight,
kCGBlendModeHardLight,
kCGBlendModeDifference,
kCGBlendModeExclusion,
kCGBlendModeHue,
kCGBlendModeSaturation,
kCGBlendModeColor,
kCGBlendModeLuminosity,
kCGBlendModeClear, R = 0
kCGBlendModeCopy, R = S
kCGBlendModeSourceIn, R = S*Da
kCGBlendModeSourceOut, R = S*(1 - Da)
kCGBlendModeSourceAtop, R = S*Da + D*(1 - Sa)
kCGBlendModeDestinationOver, R = S*(1 - Da) + D
kCGBlendModeDestinationIn, R = D*Sa
kCGBlendModeDestinationOut, R = D*(1 - Sa)
kCGBlendModeDestinationAtop, R = S*(1 - Da) + D*Sa
kCGBlendModeXOR, R = S*(1 - Da) + D*(1 - Sa)
kCGBlendModePlusDarker, R = MAX(0, (1 - D) + (1 - S))
kCGBlendModePlusLighter R = MIN(1, S + D)
*/
}
// 更新进度的时候执行重绘
- (void)setProgress:(CGFloat)progress{
_progress = progress;
// 执行重绘
[self setNeedsDisplay];
}
@end
控制器下更新歌词方法中计算平均进度,并给Label的progress属性赋值
// 更新歌词
- (void)updateLyric{
// 当前歌词
JSLyricModel *currentLyric = self.lyricModelArray[self.currentLyricIndex];
// 下一句歌词 ( 2.判断越界问题)
JSLyricModel *nextLyric = nil;
if (self.currentLyricIndex == self.lyricModelArray.count - 1) {
// 创建一个最大的下一句歌词
nextLyric = [[JSLyricModel alloc]init];
// 给自定义出来的最后一条歌词设置数据 (设置成最后一条歌词的数据)
nextLyric.content = currentLyric.content;
// 因为当前索引已经是最后一条歌词,所以上面的歌词赋值就相当于nextLyric.content = [self.lyricModelArray lastObject].content;
// 直接设置成歌曲的总时长
nextLyric.initialTime = [JSMusciManager sharedMusicManager].duration;
}else{
nextLyric = self.lyricModelArray[self.currentLyricIndex + 1];
}
// 正向调整进度(判断越界问题): 判断时间,改变当前的歌词的索引 : 当前播放时间 > 下一句歌词的起始时间 歌词索引 +1
if ([JSMusciManager sharedMusicManager].currentTime > nextLyric.initialTime && self.currentLyricIndex < self.lyricModelArray.count - 1) {
self.currentLyricIndex++;
// 拖拽进度条时,只需要显示最近当前歌词,防止拖动歌词逐条跳动
[self updateLyric];
// 1. 当累加到正确的当前歌词索引时,下面才给歌词赋值,否则递归调用返回
return;
// 如果不进行递归调用直接return: 这里更新数据的定时器间隔时间为0.1s,假如将进度条拖拽到歌词索引60的位置,那么等到定时器自动调用到到歌词索引为60的歌词数据时,需要6s的时间才可以
}
// 反向调整进度(判断越界问题): 当前时间 < 当前句歌词的初始时间 歌词索引-1
if ([JSMusciManager sharedMusicManager].currentTime < currentLyric.initialTime && self.currentLyricIndex > 0) {
self.currentLyricIndex--;
[self updateLyric];
return;
}
// 设置歌词
self.verticalLyricLabel.text = self.lyricModelArray[self.currentLyricIndex].content;
self.horizonLyricLabel.text = self.lyricModelArray[self.currentLyricIndex].content;
#pragma mark -- 设置歌词变色
/* 设置歌词变色进度
平均速度进行计算 : (当前播放时间 - 当前句起始时间) / 当前句总时间
当前句总时间 : 下一句的起始时间 - 当前句的起始时间)
*/
CGFloat averageProgress = ([JSMusciManager sharedMusicManager].currentTime - currentLyric.initialTime) / (nextLyric.initialTime - currentLyric.initialTime);
self.horizonLyricLabel.progress = averageProgress;
self.verticalLyricLabel.progress = averageProgress;
}
为了将功能模块独立出来,所以每个小的功能都封装了一个方法
updateLyric(更新歌词方法)会在updateData(更新数据的方法)中调用
updateData是一个定时器计时调用的方法
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(updateData) userInfo:nil repeats:YES];