Core Text 富文本编辑
在 Core Graphics
后面一篇文章本该对 Core Image
框架进行整理,但是基于 Core Graphics
的富文本编辑 Core Text
框架更方便讲述。而且结合自己兴趣和好玩的程度在讲述自己对 Texture
即 ASDK
和 YYKit
详细描述。
Core Text 结构层级
这里小编对 Core Text
在实现的层次结构上所处位置,以及在对 UIKit
框架我们常见控件 UITextFiled
、UITextView
和 UILabel
具体实现基础。
上图是小编根据 2018 WWDC 中 TextKit Best Practices 和 Introducing Text Kit 得出
Core Graphics
、Core Text
和UIKit
中能够实现文本绘制的控件实现结构图。
根据上面结构可以看出:
(1)基于Quartz
封装的Core Graphics
为Core Text
的实现基础。而且Quartz
可以直接处理字体和字形将文字渲染到界面,也是在 Apple 基础库中唯一一个处理字体模块。
(2)基于Core Text
封装实现的Text Kit
为在UIKit
和AppKit
中文本显示控件TypeTextFiled
、TypeTextView
等提供最直接的接口。
Core Text API 族
Core Text
作为在OS
对应的五大平台唯一拥有实现文字绘制能力的跨平 台框架,族类API
都是表示CTType
形式。
Opaque Types
集合数据类型 | 表示 & 使用范围 |
---|---|
CTFont (字体) |
1、 表示字体类型参数。字体特征中基本参数表示:字体大小、字体所属的格式或者是转换矩阵等。2、 是在 UIKit 中我们经常使用设置子类类型的 UIFont 的 (__birdge CTFont *) 表现形式,在采用 context 绘制上下文中绘制确定字体。 |
CTFontCollection (字体组) |
1、 表示字体组合。字体组合也即是对一组文字或者一段短文字字体集合,提供对同一段文字不同格式文字内容字体访问和获取。一句话:字体整体表示。 |
CTFontDescriptor (字体描述) |
1、 表示字体类型描述。字体描述可以用获取、指定或者是修改当前字体属性,字体相关属性(字体名字、位置点大小和变化等)。 |
CTFrame (绘制画布) |
1、 表示多行文字绘制画布。CTFrame (绘制画布)是 Core Text 实现文字绘制对外集中体现形式,其中确定绘制参数:画布区域(Range )、绘制路径(Path )和绘制文本参数信息(Attribtes )。2、 对于单行来说可以精确获取在画布:行数和每一行对应开头坐标。在获取绘制的画布时调用对应的 CTFrameDraw(CTFrame, CGContext) 在上下文中绘制文字显示详细内容。 3、 CTFrame 不仅支持在 main thread 中进行绘制,同样也支持在 background Thread 进行内容的绘制。 这也是 YYKit 和 Texture 支持后台绘制控件实现的基础。 |
CTFramesetter (画布🏭) |
1、 表示字体具体绘制生成工厂。画布工厂根据要显示的富文本信息、显示画布路径和画布具体的区域(Range )来生成对应展示的显示画布的效果。2、 CTFramesetter (画布工厂)是采用 CFFrameDraw 来实现富文本编辑绘制基础,但是如果采用 CTLineDraw 或者是 CTRunDraw 形似就另当别论了。 YYText 中绘制的就是按照 CTLineDraw 和 CTRunDraw 来绘制的。 |
CTGlyphInfo (字体信息) |
1、 表示在字体对于 Glyph ID 特殊的映射关系。2、 。 |
CTLine (画布中行) |
1、 表示在具体执行 Frame (画布)中一行。是组成画布绘制 Frame 中单独的一行,同时也是字体不同格式组成在一行中组成最小单元 Run (块)的集合。 2、 在实现富文本绘制的过程中可以采用 CTLineDraw(CTLine, CGContext) 方式来绘制当前行。 |
CTParagaraphStyle (段落格式) |
1、 段落格式表示在文本绘制时,段落段落之间设置基本参数或者单独一段信息基本格式。例如:对其样式、截取样式和排布的方向等等2、 。 |
CTRun (块) |
1、 表示在富文本绘制过程中格式相同最小单元。根据字体设置的 CTFont (字体)参数不同在绘制过程中可以分割为格式一致一个一个单元,既是 Run 。 2、 在文本编辑过程中可以根据在画布中需要显示 Lines ,然后在但单独 Line 中获得 Run 采用 CTRunDraw(CTRun, CGContext) 来实现单个 Run (块)独自绘制。 |
CTRunDelegate (块协议) |
1、 表示在运行时的一个运行委托,在实现计算是可以调整字形上升、下降和当前绘制字形宽高。这个 API 是我们在实现文字图片换混排的基础,在生成 Frame 时计算当前 Line 里面 Run 需要绘制素材类型,然后在改素材类型位置设置 Image 的 Ascent 、Descent 、Width 和 Height 来设置当前图片绘制参数信息,然后在实际绘制中在当前需要绘制的 Image 采用 CGContextDrawImage(CGContext, CGRect, CGImageRef) 绘制当前图片。 |
CTTextTab (文本标签) |
1、 表示在文本段落中样式标签,用来保存段落之间段落对其方式和位置信息。 |
CTTypesetter (排版工厂) |
1、 表示字体具体绘制来执行布局。可以通过 CTFramesetterCreateWithTypesetter(CTTypesetter) 生成上文展示 CTFramesetter (绘制工厂)。 |
Reference
集合数据类型 | 表示 & 使用范围 |
---|---|
Core Text Sting Attributes (富文本展示样式) |
富文本下划线样式设置 。 |
Core Text Structure (结构) |
CTTextTab (标签) 范围和表,查找向量 Header。 。 |
Core Text Enumerations (枚举) |
Core Text 字体描述匹配、指定绑定标识符自动激活、指定 URL 字体注册失败、定义字体注册范文等等。 。 |
Core Text Constants (常量值) |
Core Text 中使用富文本设置一些常量值。 |
Core Text Fundations (功能) |
Core Text 中一些功能参数值。 |
Core Text Data Types (数据分类) |
Core Text 中 ATS 字体参考、字体集合排序描述符回调和 CT 描述符处理进度回调。 |
上面是
Core Text
所有的API
的接口,及其在实现富文本绘制中相对相应的API
的主要功能作用。具体CTFramesetter
怎么样通过NSString
来生成CTFrameRef
然后在UIKit
基础的显示控件上绘制出来的呢?
Core Text
最小系统
这里说的最小系统概念是在电子单片机中最小系统单元,这里指的是 Core Text
实现最基本文字排版。
下面贴出在实现中经典代码:
1 | - (void)drawRect:(CGRect)rect { |
继承
UIView
来自定义FYView
然后重写drawRect:
,在上面drawRect:
重新写上面代码。在Core Text
实际绘制中主要分为:计算转换在UIKit
的坐标、设置绘制Path
、初始化CFFramesetter
画布工厂生成画布和在上下文中绘制。
具体分为 6 个步骤:
Step 1:获取当前绘制上下文context
;
Step 2:把Core Text
中左下角的坐标点转换为UIKit
左上角的坐标点;
Step 3:设置绘制的path
路径,把当前UIView
的bounds
设置为绘制区域;
Step 4:通过String
初始化NSAttributedString
来创建CTFramesetterRef
然后根据画布工厂创建CTFrameRef
;
Step 5:在上下文中绘制画布内容;
Step 6:释放创建frame
、path
和framesetter
的CFTypeRef
对象。
在当前 Core Text
绘制的基础之上,我们在看一下具体绘制实现中 CTTypeRef
中 Framesetter
、Frame
、 Line
和 Run
之间的关系,以及在实际绘制显示在界面上对应。
小编在实现图文混排实现中,贴出下面一段对于富文本 CFFrameRef
来计算 Image
富文本绘制布局代码。
1 | //获取 frameRef 中 Lines |
从上面两段代码可以看出在
CTTypeRef
类族中类的关系如下:
(1)CTFramesetter
通过初始化NSAttributedString
来创建绘制画布CTFrameRef
对应的类;
(2) 通过画布CTFrame
可以获取所有的Line
基本信息,例如:行数、每行Start Point
等参数。再通过坐标转换就可以计算当前Line
在UIKit
界面上布局绘制信息,在实际的绘制时可以选用CTLineDraw(CTLineRef, CGContext)
来替代CTFrameDraw(CTFrameRef, CGContext)
整个画布在当前上下文中绘制;
(3)效仿从CTFrameRef
中获取对应的Line
,同样可以在Each Line
中来获取Core Text
最基本的单元CTRunRef
。通过获取的Each Line
中所有的Run
,然后借助CTRunDelegateRef
在实际绘制过程中动态设置当前Run
块需要绘制Rect
区域。同样在实际绘制时也可以采用CTRunDraw(CTRunRef, CGContext)
来代替CTLineDraw(CTLineRef, CGContext)
来单独绘制每个字形块。
在图文混排过程中分为两种情况,根据情况的不同也可以采用不同的方式实现排版引擎实现:
(a)对于排版的数据来源于Server
也就是我们需要需要访问后才会获取数据,然后来初始化控件,鉴于Network
的延迟性,我们可以预先设置通过设置CTRunDelegate
拓展在展示时的Image
的参数信息;
(b)对要排版的数据信息已知,在实现的基础上对Image
位置插入临时代替的字符串,通过CTDelegateRef
来获取对应Image
基本参数设置当前Run
块的绘制时上下缩进,然后添加到富文本字符串中此时也可以记录当前插入字符串所在的位置。
Core Text 图文混排实现
目前以 Core Text
为基础实现图文混排实现控件 YYKit
系列组件中 YYText
实现逻辑最为清晰,瞒住的情况也最为全面。下面根据要实现图文混排的数据来源做区分来分别讲解排版引擎实现逻辑。
来自 Server
当需要排版的数据来自与 Server
,客户端和后台来商量在模板传输数据格式。这里在本地模拟网络数据加载的数据格式采用 JSON
来实现,不过小编建议如果后台允许的情况下可以尝试 Protobuf
数据格式(后面小编会在网络协议中对该格式的数据进行详细的讲解)。
这里对 Core Text
绘制实现步骤进行划分,然后对每一个步骤的工作进行提取来实现不同的功能。下面区分
(1)
FYEvolveDisplay
:显示类,用于在富文本实际绘制,排版的图片的图片实现填充和图片及链接文本点击操作监听;
(2)FYFrameParser
:排版类,对需要排版的内容加载解析,然后生成排版;
(3)FYFrameParseConfig
:配置类,在排版绘制中文字基本参数的model
配置参数;
(4)FYCoreTextData
:模型类,作为实际绘制中数据显示承载体;
(5)FYCoreTextImageData
:图片配置类,在排版绘制中图片基本参数的model
配置参数;
(6)FYCoreTextLinkData
:链接文本配置类,在排版绘制中链接文字基本参数的model
配置参数;
(7)FYCoreTextLinkUtilts
:链接文本工具类,在FYEvolveDisplay
点击中查找判断当前点击在在具体哪个链接文字。
下面按照(1)数据加载解析生成显示承载 –> (2)然后在绘制 –> (3)最后在点击相应步骤来贴出相关代码段
数据解析
1 | //FYFrameParser |
上面是模拟网络请求数据模板加载本地模板生成显示载体(即画布)过程。
Step 1: 由Step 2
加载本地模拟数据,然后由Step 5
来计算绘制画布的区域和路径生成对应画布交给模型类在自定义的UIKit
控件drawRect:
完成绘制;
Step 2: 获取本地的数据然后解码,遍历其中数据内容解析对应Text
、Image
和LinkText
数据。如果是Text
类型由Step 3
来根据内容配置来生成对应的NSAttributedString
数据,如果是Image
类型就交由Step 4
临时填充单个字符串并且通过CTRunDelegateRef
来在运行时设置当前上下缩进,如果是LinkText
类型的数据还是由Step 3
来完成处理,但是记录当前NSAttributedString
的Start Index
和End Index
位置;
Step 3:此步骤是解析Text
类型的数据,根据设置的FYFrameConfig
配置的基础信息对数据进行解析生成对应NSAttributedString
类型的数据;
Step 4:此步骤是对Image
类型的数据类型对应的CTRunRef
进行临时赋值一个字符,然后通过CTRunDelegateCallbacks
来设置Run
(块)在实际绘制时Ascent
(排版上升) 和Descent
(排版下降)间距以此来作为图片绘制时的高度。然后在实际绘制过程中遍历当前Run
(块) 计算当前Image
绘制的区域,获取图片后采用CGContextDrawImage(CGContext, CGRect, CGImage)
绘制当前图片;
Step 5:通过NSAttributedString
来生成CTFrameRef
画布工厂,计算当前需要绘制内容高度由Step 6
来生成CTFrameRef
(画布)然后赋值给FYCoreTextData
模型类画布;
Step 6: 利用最小单元中通过设置路径使用CFFramesetterRef
(绘制🏭)来生成对应需要排版的CTFrameRef
(画布)。
实际绘制
1 | //FYEvolveDisplayView |
1 | //FYCoreTextData |
>
Step 1:上面是在
FYEvolveDisplayView
中drawRect:
采用CTFrameDraw(CTFrameRef, CGContext)
绘制在UIView
上,然后从FYCoreTextData
中逐个绘制FYCoreTextImageData
取出在Step 2
计算当前Image
对一个的区域。然后使用CGContextDrawImage(CGContext, CGRect, CGImageRef)
绘制;
Step 2:这里在上文中有展示。主要一点:CTFrameRef
获取Line
每行,然后在遍历每行的Run
(块)判断其CTRunDelegateRef
对应的协议计算当前Image
对应的区域。
图片和链接文字点击
1 | //FYEvolveDisplayView |
1 | //FYCoreTextLinkUtils |
Step 1:在当前
FYEvoloveDisplayView
添加UITapGestureRecognizer
手势识别,来识别点击的位置。遍历对应FYCoreTextData
中图片Rect
来查找点击位置是否对应Image
,同时执行Step 2
来查找点击是否对应链接文字;
Step 2:在CTFrameRef
遍历每一行由Step 4
来获取对应行Rect
转变为UIKit
坐标,在该行Rect
在当前包含点击Point
时,在由Step 3
遍历对应输出FYCoreTextLinkData
链接文字;
Step 3:在点击每行Line
中通过点击文字Line
中的Index
遍历链接文字对应Range
获取对应点击FYCoreTextLinkData
;
Step 4:通过当前行Start Point
然后获取当前Run
对应Rect
然后计算出对应Line
的Rect
(区域)。
上面代码的 Demo
本地
在本地获取图文混排的数据,具体内容绘制的以 YYText
实现来进行讲述。这里仅仅展示 YYText
的类图,然后贴出提出几个问题。
YYText
类图
下面主要解决一下问题
(1)怎么提供后台绘制的能力?
(2)怎么计算String
、Image
和UIView
实现图文排序?
(3)怎么实现String
设置Text
各种格式设置?
(4)怎么实现具体绘制?
(5)怎么实现富文本上点击事件?
怎么提供后台绘制的能力
1 | //YYText |
1 | //YYTextAsyncLayer 异步后台绘制 |
1 | //YYTextAsyncLayer 绘制 |
上面贴出基于
YYTextAsyncLayer
实现在main thread
和background thread
绘制代码。可以看出两者在实际绘制的核心代码是一样的,只是在实现后台异步绘制的时候添加cancel
机制。然后通过在UIGraphicsGetImageFromCurrentImageContext()
获取当前YYText
通过YYTextLayout
绘制之后生成对应的Image
在main thread
实现didDisplay
的回调绘制。
计算 String
、Image
和 UIView
实现图文排序
这里贴出在计算 String
和 Image
,UIView
(附件) 在实际计算的实现类,由于代码量较大就不一一列出给出下面两个类。
1 | //YYTextLayout |
1 | //YYTextLine |
在
YYTextLayout
中调用YYTextLine
实例方式计算当前CTFrameRef
中Each Line
相关参数。
在YYTextLine
中可以看出以Line
为单位计算当前绘制参数,然后通过reloadBounds
来遍历当前行的Run
获取在初始化Attachment
找出当前Line
中的UIView
,CALayer
和UIImage
来设置当前Attachment
绘制区域和范围。
详细信息可以参考代码注释
实现 String
设置 Text
各种格式设置
1 | //YYTextAttribute |
1 | //NSAttributedString+YYText |
1 | + (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range { |
在初始化
String
设置字体特定的格式,然后根据提供的Key
值类型来保存在当前String
转化为NSAttributedString
类型的格式中。设置当前Line
所属的YYTextLayout
标记当前状态,在绘制时来执行在改Line
的Run
执行绘制(后面在绘制中会讲述)。
实现具体绘制?
1 | //YYLabel |
1 | //YYTextLayout |
实现富文本上点击事件
1 | //YYLabel |
参考资料:
TextKit Best Practices
Introducing Text Kit
Advanced Text Layouts and Effects with Text Kit
About Core Text
About the Cocoa Text System
About Text Handling in iOS
The Layout Manager
Advanced Text Processing
Graver
Emoji Unicode Tables
新大陆:AsyncDisplayKit
Optimising Autolayout
iOS 保持界面流畅的技巧
WebView性能、体验分析与优化
Text Kit 学习笔记
初识 TextKit
: