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

歌词视图剖析
一个体验良好的歌词视图,由以下方面组成,这也是我们的设计目标:
- 有倒计时功能,歌者可以提前作演唱的准备
- 根据场景的不同,支持多行或者双行显示,为歌者提供演唱的上下文
- 歌者清晰的了解当前在唱哪一句歌词,我称之为焦点行
- 焦点行需要染色,并需要精准地作逐字渲染
- 两句之前使用适当的动画换行过渡
- 歌词动画平滑不突兀,适应不同节奏的歌曲
- 根据产品和设计师的要求,灵活地对歌词视图进行字体、颜色调整(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 { CGRect greenRect; CGRect whiteRect CGPoint greenPoint; CGPoint whitePoint; Line *line; UIFont *font; 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]; _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。
公司简介
唱吧(最淘科技有限公司)由陈华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,或者微博私信我。此招聘长期有效,欢迎推荐及自荐。
之前做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
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。
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。