用Core Graphic做个macOS上的屏保

在写上一篇的同时我就在考虑,既然都做到了这个地步,能不能干脆移植到macOS上,做个屏保呢?

我决定试一试。

确定方案

在真正开始做之前我考察了一下Metal和OpenGL,觉得学习曲线相对“移植”这个任务来讲有点过于陡峭。由于之前从没正经做过macOS上的项目,还是觉得这次步子先迈小一点,大方向上继续沿用之前的Core Graphic方案。

由于上一篇的工程是基于Swift 3的,而如果你在XCode中创建屏幕保护的模板工程的话,会发现它并没有给你选择语言的余地,直接就给了你一个Objective-C的工程。由于程序员天生有着爱偷懒不喜欢重复造轮子的优良品性,我的第一反应是,能不能我自己手动建个Swift的工程?

但是我实验的结果是似乎目前存在兼容性问题,编译出的屏保总是莫名其妙crash。有人也遇到了同样的问题:

无奈,我还是及时止损,老老实实用Objective-C重写了遍主要的逻辑。

仅就这个项目中用到的东西而言,Cocoa和Cocoa Touch的差别比预想中的还要小,夸张点说,基本上只是把“UI”打头的东西换成“NS”就搞定了(比如UIFontNSFontUIScreenNSScreen等)。

顺便一题,默认的屏保模板是不能debug的,你需要自己手动添加一个target,然后在该target的AppDelegate里面自己把屏保的view加进来:

_rainView = [[CodeRainView alloc] initWithFrame:CGRectZero isPreview:NO];
_rainView.frame = _window.contentView.bounds;
[_window.contentView addSubview:_rainView];

但是当我费半天劲翻译完程序,真正运行起来后却发现,这货在堂堂电脑上居然跑得比在手机上更慢!

想了一下,大概的原因可能是,由于电脑的屏幕大,能同时容纳的track就更多,因此同时要刷新的track数量在运行开始后就会很快上升到可观的程度。

要是像上一篇那样,改成后台渲染呢?

试了一下,效果也不是很好。

于是,我开始琢磨换一种实现方式。

CALayer黄金搭档

考虑到这个效果的本质其实是“照亮”已经排布好的矩阵,我们可以尝试不去自己绘制字符,而是也先排布好字符,然后照亮它!

于是自然就想到CALayer家族中的两位成员和一个小弟:CATextLayerCAGradientLayermask属性。

顾名思义,他们一个用来显示字符,一个用来显示渐变,一个用来产生遮罩。

无图无真相,大概是下面这个意思:

产生字符(CATextLayer)

NSArray *characters = [[JSMatrixDataSource sharedDataSource] characters][track.trackNum];
NSString *trackString = [characters componentsJoinedByString:@""];
attrString = [[NSMutableAttributedString alloc] initWithString:trackString
                                                    attributes: [JSMatrixDataSource getStringAttrs]];
self.string = attrString;

产生遮罩(CAGradientLayer)

self.gradientLayer = [CAGradientLayer layer];
self.gradientLayer.colors = @[(__bridge id)[NSColor whiteColor].CGColor, (__bridge id)[[NSColor whiteColor] colorWithAlphaComponent:0].CGColor];
self.gradientLayer.startPoint = CGPointMake(0.5, 0);
self.gradientLayer.endPoint = CGPointMake(0.5, 1);
NSMutableDictionary *newActions = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[NSNull null], @"onOrderIn",
                                   [NSNull null], @"onOrderOut",
                                   [NSNull null], @"sublayers",
                                   [NSNull null], @"contents",
                                   [NSNull null], @"bounds",
                                   [NSNull null], @"position",
                                   nil];
self.gradientLayer.actions = newActions;

中间给actions设置的一段是为了禁用CALayer的隐式动画,因为我们此处需要的就是一跳一跳的效果。

设置蒙版(mask)

self.mask = self.gradientLayer;

1 + 2 = 3. Simple like that.

PS. 有一个小坑就是CATextLayer的刷新并不及时,因此需要我们手动清空它的内容并标记为需要刷新:

self.contents = nil;        // Force the layer to clear its content
[self setNeedsDisplay];     // Then mark the layer needs redraw

self.string = ...           // Set the new content

改进

然后我用Instrument进行了一下测试,惊讶地发现在一个简单的取屏幕最大行数的方法上居然耗费了主线程10%的时间:

为了解决这个问题,我把计算结果缓存了下来,这样以后每次取用时只需读取之前的计算结果:

+ (UInt)maxNum{
    static UInt num;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        num = ceilf([NSScreen mainScreen].frame.size.height / [JSMatrixDataSource capHeight]);
    });
    return num;
}

而在Swift上,只需把变量声明为static即可达到上述效果。

static let maxNum: Int = Int(ceilf(Float(UIScreen.main.bounds.height / JSMatrixCodeRainView.characterSize.height)))

(我真的不是在黑OC,没有任何这个意思。)

成果

大概是酱紫:

其实在运行时还是会注意到有些不自然,但更加出色的表现还是得祭出OpenGL或者Metal来做。这就留给以后了。包括还可以设置zPosition实现一些纵深感的变换效果,由于这些先天不足也懒得做了。

另外我其实完全抛弃了系统默认的屏保实现机制(在animateOneFrame方法中写动画逻辑来前进一帧),也算是个非主流的屏保……

代码已经共享到了GitHub:
https://github.com/zshowing/JSMatrixCodeRainScreenSaver

或者直接下载:
https://pan.baidu.com/s/1eRJE2P0

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341

推荐阅读更多精彩内容