基于Core Animation的KTV歌词视图的平滑实现

KTV歌词视图,只要去过KTV的的朋友一定不会陌生。我们先来看一下最终的效果,再一步步说明唱吧歌词视图的演进。想把事件事情说得清清楚楚的确很难,有很多tricky的地方;另外毕竟不是open source的,只能给大家挑重点分享一下实现的过程和思路。

唱吧6.0歌词视图

歌词视图剖析

一个体验良好的歌词视图,由以下方面组成,这也是我们的设计目标:

  • 有倒计时功能,歌者可以提前作演唱的准备
  • 根据场景的不同,支持多行或者双行显示,为歌者提供演唱的上下文
  • 歌者清晰的了解当前在唱哪一句歌词,我称之为焦点行
  • 焦点行需要染色,并需要精准地作逐字渲染
  • 两句之前使用适当的动画换行过渡
  • 歌词动画平滑不突兀,适应不同节奏的歌曲
  • 根据产品和设计师的要求,灵活地对歌词视图进行字体、颜色调整(1/3/5是绿色,2/4/6是红色,阴历节日是黄色,I’m serious and it’s safe to forget Sunday! Cheers!)

此外我们还需要了解一下歌词信息的结构,大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
@interface Line : NSObject
@property (nonatomic, strong) NSArray *words;
@property (nonatomic, strong) NSString *text;
@property CGFloat start;
@property CGFloat length;
@end
@interface Word : NSObject
@property (nonatomic, strong) NSString *text;
@property CGFloat start;
@property CGFloat length;
@end
  • 一首歌的歌词我们称之为Lyrics
  • Lyrics包含多行,每行我们称之为Line; Line有它的start及length,分别代表时间戳以及长度
  • Line包含多个字,每个字我们称之为Word; Word也有它的start以及length,分别代表时间戳以及长度

了解完这些我们看看如何来渲染焦点行歌词,先看简单直接的方式。

基于Core Graphics的实现

我们知道歌曲的开始时间,也有歌词数据提供时间支持,那么就可以计算出当前歌词视图的状态。对于歌词的焦点行,有两部分状态:

  • 歌者已经演唱的部分,渲染成绿色
  • 歌者待演唱的部分,渲染成白色

我们省略计算的过程,假设已经得出绿色、白色歌词的rect及point,就可以直接渲染了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@implementation LyricsView
- (void)drawRect:(CGRect)rect {
// Assume we calcuated them before
CGRect greenRect;
CGRect whiteRect
CGPoint greenPoint;
CGPoint whitePoint;
// We have the focus line and font
Line *line;
UIFont *font;
// Render focus line text
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextClipToRect(context, greenRect);
[[UIColor greenColor] set];
[line.text drawAtPoint:greenPoint withFont:font];
CGContextRestoreGState(context);
CGContextSaveGState(context);
CGContextClipToRect(context, whiteRect);
[[UIColor whiteColor] set];
[line.text drawAtPoint:whitePoint withFont:font];
CGContextRestoreGState(context);
}
@end

本质上我们使用了NSString的UIStringDrawing Category搞定了这个事情。既然我们解决了任一时间点的状态,那么把它动起来也很容易:

  • 将这段code snippet放到LyricsView的drawRect中
  • 以60 FPS的频率调用[lyricsView setNeedsDisplay]

一切看起来很直观,但问题来了,这个歌词视图根本跑不到60 FPS(我保证这个效果看起来像癫痫一样儿,v4.9之前就是一直这么癫过来的),即使在目前性能最强的iPhone 5S上。我们来分析一下原因:

  • Core Graphics使用CPU作渲染
  • 这个界面是CPU intensive,需要播放伴奏,还需要录制歌者的声音,甚至需要给声音加“滤镜”
  • 还有对歌者进行实时打分的task及动画
  • 回望过去5年iPhone的硬件发展,GPU的提升也远高于CPU,不能指望短期设备升级解决这个问题

5S上毕竟还可以跑到50FPS,但低端设备的FPS对我来讲是实在是没法接受的。唱吧是线上KTV的应用的用户体验标准,不解决这个问题是说不过去的。既然CPU不给力,那么我们让GPU来做这件事情。

基于Core Animation的实现

14年初的时候,Facebook open source了惊艳的Shimmer。由于跟我设想的实现机制是相同的,直接拖了几百个shimmer view作了一下profile,在4S上都可以达到完美的60FPS。

让我们先理一下思路,看看基于Core Animation的焦点行的视图结构:

1
2
- GreenLineLabel: UILabel
- WhiteLineLabel: UILabel

没错,就是简单的把绿色的UILabel置于白色的之上,剩下的问题就是如何控制绿色的UILabel按我们的时间控制进行部分渲染。

部分渲染就是加一个mask,我们来看一下CALayer的mask property:

1
2
3
4
5
6
7
8
9
10
11
@interface CALayer : NSObject <NSCoding, CAMediaTiming>
/* A layer whose alpha channel is used as a mask to select between the
* layer's background and the result of compositing the layer's
* contents with its filtered background. Defaults to nil. When used as
* a mask the layer's `compositingFilter' and `backgroundFilters'
* properties are ignored. When setting the mask to a new layer, the
* new layer must have a nil superlayer, otherwise the behavior is
* undefined. Nested masks (mask layers with their own masks) are
* unsupported. */
@property(strong) CALayer *mask;
@end

我们可以知道,mask layer的alpha用来与CALayer的content进行alpha blending,如果alpha为1则content显示,反之不显示。受Shimmer的启发,我们可以对mask作动画,让它从左到右移动到绿色歌词的layer上,并最终与之重合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface GreenLineLabel: UILabel
@end
@implementation GreenLineLabel {
CALayer *_maskLayer;
}
- (instance)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_maskLayer = [CALayer layer];
_maskLayer.backgroundColor = [[UIColor whiteColor] CGColor]; // Any color, only alpha channel matters
_maskLayer.anchorPoint = CGPointZero;
_maskLayer.frame = CGRectOffset(self.frame, -CGRectGetWidth(self.frame), 0);
self.layer.mask = _maskLayer;
self.backgroundColor = [UIColor clearColor];
}
return self;
}

上面这段代码我们将_maskLayer的anchorPoint设置为CGPointZero,便于后面的动画计算坐标。

下面我们对_maskLayer的position作CAKeyframeAnimation动画,根据歌词数据我们可以算出每个字渲染的时间(keyTimes)和动画总时长(duration)。假设每个字是等宽的,我们可以算出_maskLayer在每一个keyTime的position,也就是values。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)startAnimation {
// Assume we calculated keyTimes and values
NSMutableArray *keyTimes;
NSMutableArray *values;
CGFloat duration;
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.keyTimes = keyTimes;
animation.values = values;
animation.duration = duration;
animation.calculationMode = kCAAnimationLinear;
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
[_maskLayer addAnimation:animation forKey:@"MaskAnimation"];
}

至此我们完成了基于Core Animation的歌词焦点行染色动画。

写在后面

很抱歉我提供的code snippet不是production ready,歌词动画是一个非常复杂的系统,很难单独抽离出来介绍给大家,所以只能管窥一豹地介绍下。

附小广告一则:唱吧iOS团队诚招iOS工程师,推荐成功即奖励6000元现金或iPhone 6一部,详见这篇blog


唱吧诚聘iOS开发工程师

公司简介

唱吧(最淘科技有限公司)由陈华Tony创立于2011年。在经历数次pivot,终于找准了手机KTV方向,是国内为数不多的用户过亿的移动互联网公司之一。
Tony作为互联网老兵(酷迅创始人,前阿里高管,搜索专家),有丰富的管理、产品和技术经验,公司高管层来自酷迅、BAT、苹果等一流公司。
唱吧在2011年曾先后获得蓝驰创投和红杉资本的A、B轮数千万美金风险投资。公司发展迅速,业务稳健,财务健康。

唱吧

团队介绍

唱吧团队有着浓厚的产品和技术氛围,无996加班文化。
作为唱吧员工,每年享受10天带薪病假,5天带薪年假,五险一金齐全。 若有紧急加班可以调休,无考勤,弹性工作制。 自带设备(Macbook)公司给予补贴。
公司员工男女比例为1:1,平均年龄不到30岁,富有朝气。 每年至少两次的全体出游,每月一次的小组TB,每周的幸运饭团。 公司有钢琴房和K歌房,午休或者下班后可以去玩,每周有免费瑜伽课程和篮球比赛。
公司目前在三元桥第三置业大厦办公,临近地铁交通方便。

第三置业

由于公司业务拓展、产品研发的需要,诚聘iOS研发人才

  • 精通iOS SDK,熟练掌握Xcode/Instrument相关开发工具
  • 熟悉常用的算法和数据结构,对设计模式有一定理解
  • 熟练掌握SVN/Git之一的SCM工具
  • 聪明严谨,有良好的编码风格和工作习惯
  • 无障碍阅读英文文档,有独立解决未知复杂技术问题的能力
  • 懂折衷,擅沟通,有团队精神

加分项:

  • 技术经验丰富,有服务器/Web开发经验,掌握任意一种脚本语言
  • 擅长音视频、图形图像处理
  • 有创业经历或有App Store上架App
  • Github开源项目(不局限于Objective-C)
  • 熟读iOS Human Interface Guideline,有PM/UED等工作经验,或对产品、交互、用户体验有深刻理解
  • 热爱音乐,喜欢K歌或者会演奏乐器

薪酬范围

唱吧提供不低于一线互联网公司如BAT的薪酬待遇,根据具体职位面议,诚邀感兴趣的朋友随时来公司坐坐。

联系方式:

请发送PDF/Docx格式简历至ewangke#gmail.com,或者微博私信我。此招聘长期有效,欢迎推荐及自荐。


iOS应用开发之十大坑队友

之前做Indie Dev都是自己坑自己,两三年也没坑出来多少花样深感惭愧。最近功力大增但不敢独享,给大家带来天下码农之《iOS应用开发之十大坑队友》。博主最近不太会说人话,找到点《大腕》中疯人院的状态。小朋友请在家长指导下选择性观看,看完了别忘记微博at我分享心得。


PS: 建议播放音乐以达到最佳阅读效果。

第十名:SCM Attack

适用于使用各种类型的SCM,或者干脆不用。以git举例。只提交编译错误的代码,或者保证App一打开就crash,不帮忙改了bug别想绕过去;不正确设置gitignore文件,彰显个性;每个commit至少几十个文件,突显代码量;Log永远是潇洒的bug fix或者fix bug,只可意会不可言传;别人用rebase咱就用merge或者反过来,总之要有自己的特色;喜欢code reivew的速来接招。

难度指数:6 杀伤指数:6 综合评定:6

第九名:Coding Style Attack

此类型攻击覆盖范围之广,无人出其右。头文件不写注释,保留一堆永远不会完成的TODO/FIXME,再穿插着保留600行间歇注释掉的有模有样儿的代码;咱是.NET背景所有property一律get/set整齐划一;偶尔再来个downloadFile/downloadFile2这样写意的命名,一细看还真的不一样儿;每行代码都写几个magic number除了你没人懂什么意思;代码风格的辨识度高,以至于blame view都是多余的。

难度指数:6 杀伤指数:7 综合评定:6.5

第八名:Multi Threading Attack

本着不过度优化的原则将运算塞满主线程,反正负责优化的多数不是自己;各种Mutable对象线程间传来传去,for循环中改改更健康;sleep/dispatch_after活学活用问题搞不定全靠它;单件就有5种写法完爆茴字(老板来壶黄酒)。

难度指数:6 杀伤指数:8 综合评定:7

第七名:Header Attack

Coding Style Attack的进化;500行的header如家常便饭,管它private还是public的直接往这里扔;除了没注释还不提供初始化方法,暴露几十个property,其中的5个要是特定值某个无参方法就没法工作;最后再把一堆有关无关的header都往Prefix.pch里扔,编译时咱就拼硬件。

难度指数:7 杀伤指数:8 综合评定:7.5

第六名:OO Attack

此门技艺博大精深,属无招胜有招的范畴;能继承就不用组合,继承层次小于3层出门不好意思跟人打招呼,基类永远只有一个唯一子类。if/else嵌套个六、七层不嫌多,一对大括号保证你一屏看不完,让鼓吹多态的学院派一边凉快去;调用super咱就随机位置随机call,姿势随心情而定。

难度指数:7 杀伤指数:9 综合评定:8

第五名:View Hierarchy Attack

此类适用于使用自定义的“容器类”那票朋友。甭管View Controller的生命周期,App启动时一并创建并称之为预加载,然后每个VC贴几十上百个视图上去,使用Reveal/Xcode View Debugging时给人一种小朋友看火车的感觉,感叹自己的屏幕不够宽;再对View hierarchy各种深度广度遍历,对第n层某个view来个强制转换。图片一定要拉伸,所有视图全透明,像素一定不能对齐;种种招数保证了iPhone 5S最多跑到30FPS,瞬间充满对iPhone 6的期待。

难度指数:7 杀伤指数:10 综合评定:8.5

第四名:Massive Attack

此招集六大门派之精华,单一使出来都不够带感。Massive VC打头阵,View Controller 5000行起;基类有什么塞什么,方法的caller count为1最好;再来几十个singleton或者工具类,头文件塞上几百个类方法,最好还都没有参数和返回值;有一种恢弘大气的感觉就对了。

难度指数:9 杀伤指数:9 综合评定:9

第三名:Dynamic/Typeless Attack

充分利用Objective-C的动态性,能用id咱就不用具体类型;API response/NSNotification/db里面数以百计的key/value飞来飞去;同一个东西在不同的地方类型和名字一定要有差异;诸如此类我们称之为了解代码熟悉业务,需要挂debugger说明代码不熟或者你太弱了;重构工具就是摆设,因为你根本用不了。

难度指数:9 杀伤指数:10 综合评定:9.5

第二名:Runtime Attack

+Load里面各种黑魔法,黑得小伙伴没人敢动;Catetory中搞些同名方法再加诡异的method swizzling。这类招数杀伤力极强,很可能数小时也定位不了问题所在,又能提升逼格,强烈推荐。

难度指数:10 杀伤指数:10 综合评定:10

第一名:Cross Demension Attack

最能坑你的永远不是队友这种同一维度的生物,你懂的

难度指数:??? 杀伤指数:??? 综合评定:???

附小广告一则:唱吧iOS团队诚招iOS工程师,推荐成功即奖励6000元现金或iPhone 6一部,详见这篇blog


WWDC 2014 PDF 及session 视频下载脚本

Long time no C. 今天为大家带来WWDC 2014 sessoin PDFs & Videos的下载脚本。

多线程下载请安装axel (brew install axel)

PDFs:

curl https://developer.apple.com/videos/wwdc/2014/ | grep -ioI 'http.*pdf?dl=1' | sed 's/\?dl=1//g' | xargs -n1 axel -a -n 4

Videos:

curl https://developer.apple.com/videos/wwdc/2014/ | grep -ioI 'http.*._hd_.*dl=1">HD' | sed -e 's/\?dl=1">HD//g'| xargs -n1 axel -a -n 8

单线程下载请替换最后的piping section为 xargs -n1 curl --remote-name

Enjoy:)

附小广告一则:唱吧iOS团队诚招iOS工程师,推荐成功即奖励6000元现金或iPhone 6一部,详见这篇blog