一、CoreText的简介
CoreText是用于处理文字和字体的底层技术。它直接和Core Graphics(又被称为Quartz)打交道。Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。
下面是CoreText的架构图,可以看到,CoreText处在非常底层的位置,上层的UI控件(包含UILable、UITextField及UITextView)和UIWebView都是基于CoreText来实现的。
UIWebview也是处理复杂的文字排版的备选方案。对于排版,基于CoreText和基于UIWebView相比,具有以下不同点:
- CoreText占用内存更少,渲染速度更快,UIWebView占用内存多,渲染速度慢。
- CoreText在渲染界面前就可以精确地获得显示内容的高度(只要有了CTFrame即可),而UIWebView只有渲染出内容后,才能获得内容的高度(而且还需要通过JavaScript代码来获取)。
- CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
- 基于CoreText可以做更好的原生交互效果,交互效果可以更细腻。而UIWebView的交互效果都是利用JavaScript来实现的,在交互效果上会有一些卡顿情况存在。例如,在UIWebView下,一个简单的按钮按下操作,都无法做出原生按钮的即时和细腻的按下效果。
当然,基于CoreText的排版方案也有那么一些劣势:
- CoreText渲染出来的内容不能像UIWebView那样方便的支付内容的复制。
- 基于CoreText来排版需要自己处理很多复杂逻辑,例如需要自己处理图片和文字混排相关的逻辑,也需要自己实现链接点击操作的支持。
1、图文混排
CTFrameRef textFrame // coreText 的 frame
CTLineRef line // coreText 的 line
CTRunRef run // line 中的部分文字
2、相关方法:
CFArrayRef CTFrameGetLines(CTFrameRef frame) //获取包含CTLineRef的数组
void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[])//获取所有CTLineRef的原点
CFRange CTLineGetStringRange(CTLineRef line) //获取line中文字在整段文字中的Range
CFArrayRef CTLineGetGlyphRuns(CTLineRef line)//获取line中包含所有run的数组
CFRange CTRunGetStringRange(CTRunRef run)//获取run在整段文字中的Range
CFIndex CTLineGetStringIndexForPosition(CTLineRef line,CGPoint position)//获取点击处position文字在整段文字中的index
CGFloat CTLineGetOffsetForStringIndex(CTLineRef line,CFIndex charIndex,CGFloat* secondaryOffset)//获取整段文字中charIndex位置的字符相对line的原点的x值
二、基于CoreText的基础排版引擎
简单实现步骤:
a.自定义View,重写drawRect方法,后面的操作均在其中进行
b.得到当前绘图上下问文,用于后续将内容绘制在画布上
c.将坐标系翻转
d.创建绘制的区域,写入要绘制的内容
示例1:不带图片的排版引擎,只是显示文本内容,而且不设置文字的属性信息
自定义的CTDispalyView.m
在ViewController.m实现显示
演示结果截图
三、基于CoreText的基本封装
发现,虽然上面效果确实达到了我们的要求,但是,很有局限性,因为它仅仅是展示了CoreText排版的基本功能而已。要制作一个比较完善的排版引擎,我们不能简单的将所有的代码都放到CTDisplayView的drawRect方法中。根据设计模式的“单一功能原则”,我们应该把功能拆分,把不同的功能都放到各自不同的类里面进行。
对于一个复杂的排版引擎来说,可以将功能拆分为以下几个类来完成:
1、一个显示用的类,仅仅负责显示内容,不负责排版
2、一个模型类,用于承载显示所需要的所有数据
3、一个排版类,用于实现文字内容的排版
4、一个配置类,用于实现一些排版时的可配置项
例如定义的4个类分别为:
CTFrameParserConfig类:用于配置绘制的参数,例如文字颜色、大小、行间距等
CTFrameParser类:用于生成最后绘制界面需要的CTFrameRef实例
CoreTextData类:用于保存由CTFrameParser类生成的CTFrameRef实例,以及CTFrameRef实际绘制需要的高度
CTDisplayView类:持有CoreTextData类实例,负责将CFFrameRef绘制在界面上。
关于这4个类的关键代码如下:
CTFrameParserConfig
CTFrameParser
CoreTextData
CTDisplayView
除了这4个类外,在代码中还创建了基本的宏定义和分类Category,分别是CoreTextDemo.pch、UIView+Frame.h(快速访问view的尺寸)
CoreTextDemo.pch
UIView+Frame.h
示例2:不带图片的排版引擎,只是显示文本内容,设置文字的一些简单的属性信息
演示结果截图
好了,效果确实是实现了,现在来看看本框架的UML示意图,这4个类的关系是这样的:
1、CTFrameParser通过CTFrameParserConfig实例来生成CoreTextData实例;
2、CTDisplayView通过持有CoreTextData实例来获取绘制所需要的所有信息;
3、ViewController类通过配置CTFrameParserConfig实例,进而获得生成的CoreTextData实例,最后将其赋值给CTDisplayView成员,达到将指定内容显示在界面的效果。
四、定制排版文件格式
对于上面的例子,我们给CTFrameParser增加了一个将NSString转换为CoreTextData的方法。但是这样的实现方式有很多的局限性,因为整个内容虽然可以定制字体大小、颜色、行高等信息,但是却不能支持定制内容中某一个部分。例如,如果我们只想让内容的某几个字显示成红色并将字体变大,而让其他的文字显示成黑色而且字体不变,那么就办不到了。
解决办法:让CTFrameParser支持接受NSAttributeString作为参数,然后在ViewController中设置我们想要的NSAttributeString信息。
更改后的CTFrameParser
示例3:不带图片的排版引擎,只是显示文本内容,通过富文本更改文字的一些简单的属性信息
演示结果截图
更进一步,实际工作中,我们更希望通过一个排版文件,来设置需要排版的文字的内容、颜色、字体大小等信息。我们规定排版的模板文件为JSON格式。排版格式示例文件如下:
通过苹果提供的NSJSONSeriallization类,我们可以将上面的模板文件转换成NSArray数组,每一个数组元素是一个Dictionary,代表一段相同设置的文字。为了简单,我们配置文件只支持配置颜色和字号,但是以后可以根据同样的思想,很方便地增加其他配置信息。
现在修改CTFrameParser类,增加如下的这些方法,让其可以从如上格式的模板文件中生成CoreTextData。最终实现代码如下:
更改后的CTFrameParser:
示例4:不带图片的排版引擎,只是显示文本内容,通过排版文件格式更改文字的一些简单的属性信息
演示结果截图
可以看到,通过一个简单的模板文件,我们可以很方便地定义排版的配置信息了。
五、支持图文混排的排版引擎
在上面的示例中,我们在设置模板文件的时候,就专门在模板文件里面预留了一个名为type的字段,用于表示内容的类型。之前的type的值都是txt,这次,我们增加一个img的值,用于表示图片。同时给img类型的内容还需要配置3个属性如下:
1、width:用于设置图片显示的宽度
2、height:用于设置图片显示的高度
3、name:用于设置图片的资源名
也即文件格式如下:
在改造代码之前,先来了解一下CTFrame内部的CTLine和CTRun。
在CTFrame内部,是有多个CTLine类组成的,每一个CTLine代表一行,每个CTLine又是由多个CTRun来组成,每一个CTRun代表一组显示风格一致的文本。我们不用手工管理CTLine和CTRun的创建过程。
CTLine和CTRun示意图如下:
示意图解释:
可以看到,第一行的CTLine是由两个CTRun构成的,第一个CTRun为红色大字号的左边部分,第二个CTRun为右边黑色小字号部分。
虽然我们不用管理CTRun的创建过程,但是我们可以设置某一个具体的CTRun的CTRunDelegate来指定该文本在绘制时的高度、宽度、排列对齐方式等信息。
对于图片的排版,其实,CoreText本质上是不支持的,但是,可以在显示文本的地方,用一个特殊的空白字符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度信息,这样最后生成的CTFrame实例,就会在绘制时将图片的位置预留出来。以后,在CTDisplayView的drawRect方法中使CGContextDrawImage方法直接绘制出来就行了。
改造模板解析类,要做的工作有:
- 增加一个CoreTextImageData类,寄存图片信息
- 改造CTFrameParser的parserTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config方法,使其支持type为omg的节点解析。并且对type为omg的节点,设置其CTRunDelegate信息,使其在绘制时,为图片预留相应的空白位置。
- 改造CoreTextData类,增加图片相关的信息,并且增加计算图片绘制局域的逻辑。
- 改造CTDisplayView类,增加绘制图片的相关的逻辑。
具体的改造如下:
新添加CoreTextImageData类:
修改CTFrameParser解析类:
改造CoreTextData类:
改造CTDisplayView类:
示例5:带图片的排版引擎,显示文本内容和图片,通过排版文件格式更改文字的一些简单的属性信息
测试效果图如下:
六、添加对图片的点击支持
实现方式
为了实现对图片的点击支持,我们需要给CTDisplayView类增加用户点击操作的检测函数,在检测函数中,判断当前用户点击的局域是否在图片上,如果在图片上,则触发点击图片的逻辑。拼过提供的UITapGestureRecognizer可以很好地满足我们的要求,所以我们这里用它来检测用户的点击操作。
这里我们实现的是点击图片后,显示图片。实际开发中,可以根据业务需求去调整点击后的效果。
CTDisplayView类实现如下,增加点击手势:
点击图片演示截图:
七、添加对链接的点击支持
实现方式:需要修改模板文件,增加一个名为”link”的类型,用于表示链接内容。格式如下:
首先增加一个CoreTextLinkData类,用于记录解析JSON文件时的链接信息:
CoreTextLinkData
接着增加一个工具类CoreTextUtils类,用于检测链接是否被点击:
CoreTextUtils:
然后依次改造CTFrameParser类,CoreTextData类,CTDisplayView类
CTFrameParser:
CoreTextData:
CTDisplayView
测试截图:
源码链接:https://github.com/xiayuanquan/CoreTextKit.git
本博文摘自唐巧《iOS开发进阶》,本人花了点时间学习并做了一下整理和改动,希望对学习这方面知识的人有帮助。