YYText 是如何工作的

Home PDF English

上面的阴影效果是用以下代码实现的:

可以看到先生成了 YYTextShadow, 然后赋值给了 attributedStringyy_textShadow,然后再把 attributedString 赋值到 YYLabel 里面,接着把 YYLabel 加入到 UIView 里来显示。跟踪 yy_textShadow 发现,主要是把 textShadow 绑定到了 NSAttributedString 的 attribute 里,key 是 YYTextShadowAttributeName,值是 textShadow,也就是先把 shadow 存起来,后来再使用。用 Shift + Command + J 快速跳转到定义处:

这里有个 addAttribute,它在 NSAttributedString.h 里定义:

- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range;

说明可以赋值任意的键值对给它。而 YYTextShadowAttributeName 的定义是一个普通的字符串,这意味着先是把 shadow 信息存起来,然后后面再使用。我们全局搜索一下 YYTextShadowAttributeName

然后我们来到 YYTextLayout 里的 YYTextDrawShadow 函数:

CGContextTranslateCTM 是说改变一个 Context 里的原点坐标,所以

CGContextTranslateCTM(context, point.x, point.y);

是说要把绘制的上下文移动到 point 点。我们还是先搞清楚哪里调用了 YYTextDrawShadow,发现是在 drawInContext 里调用的。

drawInContext 里,依次绘制方块的边框,然后绘制背景边框、阴影、下划线、文字、附属物、内阴影、删除线、文字边框、调试线。

那么到底哪里用了 drawInContext 呢?可以看到里面有个参数 YYTextDebugOption,所以这个函数一定不是系统的回调,而是 YYText 里面自己调用的。

按住 Ctrl + 1 弹出快捷键,发现有四个地方调用了它。

drawInContext:size:debug 仍然是 YYText 自己的调用,因为 debug 的类型是 YYTextDebugOption *, 是 YY 自身的。newAsyncTask 不像是系统的调用,addAttachmentToView:layer: 同理,所以极有可能是 drawRect:

果然是,看右边的快速帮助,有详尽的解释,帮助的下面也说明了是在 UIView 里定义的。再看 YYTextContainerView,它是继承了 UIView 的。

所以 YYLabel 是用了 YYTextContainerView 咯?然后让系统调用 YYTextContainerView 里的 drawRect: 画出来?

奇怪,YYLabel 可继承了 UIView。所以,YYText 里应该有两套东西!一套 YYLabel,一套 YYTextView,像 UILabelUITextView 一样。接着我们再回去看之前的 YYLabelnewAsyncDisplayTask

很长,在中间的位置调用了 YYTextLayout 里的 drawInContextnewAsyncDisplayTask,它又是在哪里调用的呢?

在第二行被调用了。所以可以简单地理解为 YYLabel 用了异步来绘制文本。而 _displayAsync 被上面的 display 调用了。看 display 的文档,说是系统会在恰当的时间来调用来更新 layer 的内容,你不要直接去调用它。我们也可以给它打个断点。

说明 display 是在 CALayer 的一次事务中调用的。为何用事务,大概是因为想批量更新,效率高点吧?不像是数据库里的回滚需求。

display 的系统文档还说,如果你想你的 layer 绘制不一样,那你可以复写这个方法,来实现你自己的绘制。

所以,我们简单的有了一点思路。YYLabel 通过复写 UIViewdisplay 方法,来异步绘制自己的阴影等各种效果,阴影效果先保存在了 YYLabelattributedText 里的 attribute 中,在 display 中绘制的时候再取出来,绘制的时候用了系统的 CoreGraphics 框架。

理清了一些思路后,会发现,真正强大的是什么?一边是把这么多效果、异步调用等组织起来,一边是对底层 CoreGraphics 框架熟练运用。所以对前面的代码组织有了些了解后,接着我们深入到 CoreGraphics 框架上去。看看是怎么绘制上去的。

让我们重新回到 YYTextDrawShadow

这里,CGContextSaveGStateCGContextRestoreGState 包围起了一段绘制的代码。CGContextSaveGState 的意思是说,把当前的绘图状态拷贝一份,放到绘制栈里。每个绘制的 Context 都维护着一个绘制栈。我也不清楚,里面栈到底是怎么操作的。先暂且理解为绘制 Context 前要调用 CGContextSaveGState,绘制 Context 后要调用 CGContextRestoreGState,之后中间的绘制就能有效地出现在 Context 里。CGContextTranslateCTM 是移动到 Context 移动到相应的位置。先是移动到 point.xpoint.y ,绘制的相应位置,至于后面移动到 0 和 size.height,倒不清楚了,后续再看看。接着取出了 lines,执行了 for 循环。

lines 是什么?发现在 YYTextLayout 里的 (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range 赋值的。

接着翻到这个函数的定义处:

这个函数非常长,367 到 861 行,500 行代码!看了头尾,可见它的用处就是得到这些变量。lines 是怎么得到的呢?

可以见到在一个大的 for 循环里把一条一条 line 加入到 lines 里。那 lineCount 是怎么得到的呢?

第 472 行创建了一个 framesetter 对象,text 参数是 NSAttributedString,接着在 frameSetter 对象中创建了一个 CTFrameRef,接着从 CTFrameRef 得到了 linesline 到底是什么呢?我们给它打个断点。

发现,shadow 这个字的 lineCount = 2,并不是我们想象中的字母个数。

所以猜测,白色的 Shadow 整个是一条 line,阴影也是一条 line

YYText 里有好几个例子,只显示其中一种效果,把其它的代码注释掉。发现很奇怪,Shadow 的 lineCount = 2,Multiple Shadows 的 lineCount 也是 2,可 Multiple Shadows 还有内阴影啊,应该是 3 条啊?

去找 CTLine 的苹果文档,说 CTLine 代表着一行的文本,一个 CTLine 对象包含着一组的 glyph runs。所以就是简单的行数而已!看上面的断点截图,刚刚 shadow 之所以为 2 ,是因为它的文本是 shadow\n\n,看刚刚,\n\n 是故意加的,为了显示美观:

所以 shadow\n\n 就是两行文本。CTLine 就是我们平时说的行。接着回去看我们的 lineCount

这里得到 CTLines 数组,从里面的个数,然后如果 lineCount 大于 0 的话,得到每行的坐标原点。好了,有了 lineCount,我们接着看 for 循环。

ctLines 数组里得到 CTLine,接着得到 YYTextLine 对象,然后加入到 lines 数组中。然后做一些 line 的 frame 计算。YYTextLine 的构造函数很简单,先保存着位置、是否垂直排版、CTLine 对象:

lines 搞清楚之后,我们再回去之前的 YYTextDrawShadow 中去:

这下代码简单了。先获取到行数,遍历它,然后取得 GlyphRuns 数组,再遍历它,GlyphRun 可以理解为一个图元,或者绘制单元。然后从中得到 attributes 数组,用我们之前的 YYTextShadowAttributeName,获取我们一开始赋值的 shadow,接着开始绘制阴影:

一个 while 循环,不断绘制子阴影。调用 CGContextSetShadowWithColor 设好阴影的位移、半径、颜色。接着调用 YYTextDrawRun 来真正的绘制。YYTextDrawRun 被三个地方调用了:

用来绘制内阴影和文本阴影以及文本。说明它是个通用方法,用来画 Run 这个对象。

一开始获取文字的变换矩阵,用 runTextMatrixIsID 来看看它是否原地不变,如果不是垂直排版或没有设置图元转换的话,就直接上来画。调用 CTRunDraw 来画 run 对象。接着断点发现,绘制一开始那个阴影时只进入了 if 里面,没有进入 else 里面。

所以我们的阴影绘制就到此结束了!

总结一下,YYLabel 先把阴影等效果保存在 attributedText 里的 attributes,复写了 UIViewdisplay 方法,在 display 中进行异步绘制,用 CoreText 框架得到 CTLineCTRun 对象,从 CTRun 获取到 attributes,之后再根据 attributes 里的各属性,用 CoreGraphics 框架把 CTRun 对象绘制到 Context 中。

理解还是不够,等后续再来品读。不觉感叹 YY 实在太强了!今天理了理思路,让自己边写边读代码,不至于枯燥,同时供大家参考。得去睡觉了。


Back 2016.05.24 Donate