富文本框架里YYText在性能方面的表现很出色,它基于 CoreText 做了大量基础处理并实现了两个上层视图组件:YYLabel 和 YYTextView。在了解富文本处理之前,我们还需要对 CoreText 基础知识做一些了解。本篇主要梳理 YYText 中 CoreText 的底层基础部分处理。
框架概述
iOS 中我们常会在主线程中进行 UI 的绘制,但当绘制压力过大时会造成页面卡顿情况的出现。一种解决思路是,通过多线程在异步线程进行图形的绘制,以减轻主线程的压力。
YYText 框架的实现思路也是这样的。
- 创建自定义绘制线程
- 在该线程中创建图形上下文
- 通过 CoreText 绘制富文本,通过 CoreGraphics 绘制图片、阴影、边框等
- 最后将绘制完成得到的位图,回到主线程展示
CoreText 工具类
关于 CoreText 的结构图:
YYTextRunDelegate
富文本中为定制一段区域的大小,可以在富文本中插入 key 为 kCTRunDelegateAttributeName
的 CTRunDelegateRef
实例。通过这种方式来预留空白,以用来填充图片,进行图文的混排。作者可能考虑到 CFRunDelegateRef 本身的使用会比较繁琐,为了简易使用,进行了封装。也就是这里的 YYTextRunDelegate。
内部实现的思路:
- 通过
CTRunDelegateCreate()
创建一CTRunDelegateRef
- 通过
__bridge_retained
转移内存管理,持有一个YYTextRunDelegate
对象
一些细节处理:
- 内存管理问题。
CTRunDelegateRef
实例持有YYTextRunDelegate
,当CTRunDelegateRef
实例释放时,会调用DeallocCallback()
,将内存管理权限转移给YYTextRunDelegate
局部变量的 ARC。 - CTRunDelegateCreate() 里做了 copy 操作。这里是深拷贝,目的是为了创建一个副本,以保证配置数据的安全性,不会被篡改。
YYTextLine
创建一个富文本,拿到 CTRunRef、CTLineRef 及一些结构数据(如 ascent、descent)。
line 位置及大小的计算
// 不考虑竖排版 |
- _position 指 line 的 origin 点位于 context 上下文的坐标转换为 UIKit 坐标系的值。
_position.y - _ascent
表示 y 起始位置_ascent + _descent
表示 line 高度_firstGlyphPos
表示第一个 run 相对 line 的偏移
找出占位 run
基本原理是通过 CTRunDelegateRef
占位,再用 YYTextAttachment
填充。当遍历 line 里的 run 时,若该 run 内有 YYTextAttachment
,说明是占位 run,计算这个 run 的位置和大小,便于后面填充。
runPosition.x += _position.x; |
runPosition
表示相对 line 的 x/y 偏移量- 最后,缓存
YYTextAttachment
和 run 位置大小信息
YYTextContainer
CTFrameRef 需要通过 CTFramesetterCreateFrame() 创建,该方法需要 CGPathRef
作为参数,作者封装了 YYTextContainer
类来简化使用。
/* |
开发者可以通过 CGSize 来设定富文本大小,也可以通过 UIBezierPath 定制路径。同时,CoreText 还支持镂空效果,通过 exclusionPaths 控制。
YYTextLayout
包含了布局一富文本所有的信息,这个文件里也包含很多绘制相关的 C 代码。YYTextLayout 负责计算各种数据,为后面的绘制做准备。
核心计算方法:
+ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range; |
计算绘制路径和路径的位置矩形
基于 YYTextContainer 对象计算得到 CGPathRef。UIKit 转为 CoreText 坐标,需要先进行坐标处理。得到 pathBox
,pathBox 是真正的绘制区域相对于绘制上下文的位置和大小。在后面计算 line 和 run 位置时,都需要这里的 cgPathBox.origin
。
初始化 CTFramesetterRef 和 CTFrameRef
计算 line 总 frame
- 前面 TextContainer 得到
CTFrameRef
- 接着遍历所有的 line,结合
cgPathBox.origin
计算得到每个 line 的位置和大小 - 最后将每个 line 的 rect 合并,得到包含所有 line 的最小位置矩形
textBoundingRect
。
计算 line 的行数
在有排除路径的情况下,一行可能有多个 line。所以需要计算每个 line 所在的行。
当 line 高度大于 lastline 高度时。若 lastline 的 baseline 在 line 的 y0~y1 之间,说明未换行。
当 line 高度小于 lastline 高度时。若 line 的 baseline 在 lastline 的 y0~y1 之间,说明未换行。
确定了 line 的换行规则,可以计算得到 line 的行数。
获取行上下边界的数组
上下边界结构体:
typedef struct { |
YYRowEdge 表示每一行的上下边界。遍历所有 line,当当前 line 和 last line 为同一行时,取 line 和 last line 最大上下边界;当当前 line 和 last line 不同行时,取当前 line 的上下边界。
结果如图示:
中间的间隙为行间距,YYText 将行间距进行了均分,如图示:
计算绘制区域的总大小
前面通过 YYTextContainer
计算得到绘制路径的位置矩形 pathBox(上图蓝色区域)。但这是实际绘制区域的大小,但应用场景中还会有 inset、borderWidth 之类的情况。所以实际业务需要的绘制区域会更大。
line 截断
当富文本超过限制时,通常会看到文本最后有点省略号:text...
。YYText 支持自定义后缀,即 truncationToken
。
YYTextLine 总是在富文本最后,当 lastLineText 超出绘制范围,通过 CTLineCreateTruncatedLine(...)
创建自动计算的截断 line,会返回一个 CTLineRef
,框架将其转化为 YYTextLine
作为 YYTextLayout 的一个属性 truncatedLine
。
自定义富文本属性
原理是遍历富文本,找到某个 run 是否包含自定义的 key,接着做对应的绘制逻辑。
图文混排的实现
前面提到过,如果想要在富文本中添加 UIImage、UIView 之类的附件,需要先设置一个占位符 CTRunDelegateRef
,具体看下占位的逻辑。
对齐方式
图文混排添加附件有三种对齐方式:居上对齐、居中对齐、局下对齐。通过 ascent、descent、baseline 控制。
- 居上对齐
run 的 ascent 对齐文本的 ascent。
- 居下对齐
run 的 descent 对齐文本的 descent。若 run 太矮,则居上对齐文本的 baseline。
- 居中对齐
居中相对会复杂些,run 的 center 和文本的 center 对齐。
center = (ascent + descent) / 2 |
若 run 太矮,则贴着 baseline。
绘制附件
绘制逻辑在 YYTextLayout 的 YYTextDrawAttachment(...)
方法中。
- 若附件为 UIImage,会根据占位 run 的位置和大小,通过 CoreGraphics API 绘制图片:
CGImageRef ref = image.CGImage; |
- 若附件为 UIView、CALayer,需要传入额外的 superView、superLayer。再将绘制的 UIView、CALayer 添加到 superView、superLayer。
点击高亮的实现
YYTextHighlight 包含单击和长按的回调、及一些显示的属性配置。
如 YYLabel 中的触发逻辑。先判断点击的 CGPoint 对应的富文本位置,检测文本是否有对应的手势处理。若有对应的 YYTextHighlight 处理,则更换 YYTextLine 为高亮 YYTextLine,重绘。松手时,恢复 YYTextLine。
简而言之,通过检测点击 CGPoint 是否有对应的手势处理,若有替换对应的 YYTextLine,重新绘制。
总结
本篇主要是对 YYText 中 CoreText 处理的原理做了梳理,从 CTFrameRef 到 CTLineRef 到 CTRunRef。接着基于 line 和 run 做具体的展开,如:
- line 的行数确定
- line 的换行规则
- line 的截断
- run 的对齐
- 富文本附件的添加原理