CoreText跳坑

记得上次更新是半年前了,转眼就到了18年的Q3了。回顾一下我的博客的空窗期,记得在年初的时候前东家孵化了个线上的视频约见的项目,自己也算是尽力在做。之后又对项目管理感兴趣学了些报了个项目管理的考试,然而在最后一门论文崩盘也是心塞。在工作学习中发现,自己接触的越多越会感觉到自己的渺小自己的不足,同时考完试之后跳了一家更大的公司也搬了家,在项目组里面默默无闻,尽力去做的更好去完善自己吧,咳咳,扯多了,作为回归的第一篇还是得有点质量,总结一下CoreText的相关的东西,尽量用最少的文字讲更多的东西

Core Text

OverView

Core Text提供了一个底层的编程接口,用于文本的布局和处理字体。Core Text布局引擎旨在实现高性能,易用性以及与Core Foundation的紧密集成。文本布局API提供高质量的排版,包括字符到字形的转换,带有连字,字距调整等。互补的Core Text字体技术提供自动字体替换,字体描述符和集合,轻松访问字体度量和字形数据以及许多其他功能。

多线程中的注意事项:Core Text中的所有单个函数都是线程安全的。字体对象(CTFontCTFontDescriptor和相关的对象)可以被多个任务、队列或线程同时使用。但是,布局对象(CTTypesetterCTFramesetterCTRunCTLineCTFrame和关联对象)应该在单个操作,工作队列或线程中使用。

基本概念

坐标系相关

我们如果不做任何转换的话我们应该绘制的是这个样子的

1.png

可能图有点小,大致就是y轴反向的样子,因为Core Text期初是OS X系统上面的底层库,y轴为向上的方向,原点坐标是在左下,而iOS的原点坐标是在左上角的,y轴正好是反方向

Core Text相关概念

今天我们尽量不去涉及Text Kit相关的东西,我们只是借用一下图,Text Kit是对Core Text的更高级的封装。

如下图我们可以了解到Core Text的形成层次结构,最上层的是CTFramesetter工厂对象CTFramesetterRef可以理解为负责一个区域的图文绘制的对象,随后这个工厂对象会根据属性字符串去调用CTTypesetter对象去做排版相关的操作,同时根据属性字符串CTFramesetter对象生成若干CTFrame段落对象,每个段落对象又生成若干的CTLine对象可以理解为每个段落中的行的对象,最后每个CTLine行对象中又生成若干CTRun字形运行时对象,每个对象里面的每个字形都有相同的属性,比如一行6个字里面cccccc,每个字母都不一样的颜色那这个CTLine对象就有6个CTRun对象

可以看相关的文档做更深入了解

还有这里

基本Class

CTFont

CTFont用来表示Core Text字体对象。字体对象表示应用程序的字体,CTFont提供对字体描述的访问,例如磅值,变换矩阵属性和其他属性。字体提供了相对于彼此布置字形的帮助,并用于在图形上下文中绘制时设置字体。

CTFontCollection

CTFontCollection用来表示字体描述对象集合,即一组字体合成的单个对象。

CTFontDescriptor

CTFontDescriptor用来表示字体描述对象,可以指定字体的属性字典(例如名称,磅值和变体)。

CTFrame

CTFrame用来表示包含多行文本的框架。帧对象是由框架集对象执行的文本框架处理产生的输出。

CTFramesetter

CTFramesetter类型用于生成CTFrame对象。CTFramesetter是CTFrame对象的对象工厂。

CTGlyphInfo

CTGlyphInfo类型让你可以覆盖字体从Unicode到字形ID的指定映射。

CTLine

CTLine用来表示一行文本。

CTParagraphStyle

CTParagraphStyle用来表示属性字符串中的段落或标尺属性对象。

CTRun

CTRun用来表示字形运行,它是一组拥有相同属性和方向的连续字形。

CTRunDelegate

CTRunDelegate 用来表示一个运行代理,其被分配到一个运行(属性范围),以控制印刷性状例如字的向上偏移量,字的向下偏移量,和字宽。

CTTextTab

CTTextTab用来表示段落样式中的选项卡,存储对齐类型和位置。

CTTypesetter

CTTypesetter用来表示排版器,它执行行布局。

查看详细文档

Demo

简单的绘制文字

因为Core Text底层是使用Core Graphics来绘制的,我们要在UIViewdrawRect中写相关的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//1
CGContextRef context = UIGraphicsGetCurrentContext();

//2
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path , NULL , self.bounds);

//3
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context , 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

//4.
NSAttributedString *att = ...;

//5.
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)att);

//6.
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [att length]), path , NULL);

//7.1
CTFrameDraw(frame, context);
//7.2
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex lineNumber = CFArrayGetCount(lines);
CGPoint lineOrigins[lineNumber];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0 ; i < lineNumber; i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGContextSetTextPosition(context, lineOrigins[i].x, lineOrigins[i].y);
CTLineDraw(line , context);
}
//7.3
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex lineNumber = CFArrayGetCount(lines);
CGPoint lineOrigins[lineNumber];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0 ; i < CFArrayGetCount(lines); i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGContextSetTextPosition(context, lineOrigins[i].x, lineOrigins[i].y);
CFArrayRef runs = CTLineGetGlyphRuns(line);
CFIndex runNumber = CFArrayGetCount(runs);
for (int j = 0; j < runNumber; j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
CTRunDraw(run, context, CFRangeMake(0, 0));
}
}

//8
CFRelease(path);
CFRelease(framesetter);
CFRelease(frame);

代码详解

1

应该不用过多的解释,就是因为drawRect方法内部是由Core Graphics来做绘制任务的,再次可以取到当前绘制的上下文对象CGContextRef。虽然在其他的地方可以自己去创建CGContextRef上下文对象,然后自己去绘制,然后再释放,但系统在drawRect绘制方法的时候会压栈进去一个系统创建的CGContextRef上下文对象,我们为什么不去用呢,毕竟写Core Graphics的代码也是很刺激,如果动态性很强建议不要在drawRect中重写相关的代码,因为一旦重写了这个方法系统会自动给你分配一块内存去做绘制任务,大家可以使用断点打印一下self.layer.contents,当没有实现drawRect方法的时候是nil

咳咳,跑题了,大家可以看下面两个博客简单了解一下

绘制像素到屏幕上

内存恶鬼drawRect(其实也不至于题目说的那么严重…)

1
2
CGContextRef context = UIGraphicsGetCurrentContext();

2

这里我们可以对绘制区域做更细化的限制,如果Core Graphcis用着不习惯就用UIBezierPath来限制绘制区域吧,请注意这里面使用的仍然是系统坐标,如果需要绘制被排除的区域,需要使用系统坐标

1
2
3
4
//初始化绘制的区域
CGMutablePathRef path = CGPathCreateMutable();
//绘制形状限制为矩形
CGPathAddRect(path , NULL , self.bounds);

3

坐标系相关概念如果还不是很理解可以去前面再看一下

灰机票

1
2
3
4
5
6
//设置字形的变换矩阵不做图形变换
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//将画布向上平移相应的高度
CGContextTranslateCTM(context, 0, self.bounds.size.height);
//x轴缩放系数为1,y轴缩放系数为-1,以x轴为轴旋转180度
CGContextScaleCTM(context, 1.0, -1.0);

4

不解释

5

1
2
//初始化framesetter工厂对象
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)att);

6

这也有多种写法,如果喜欢用UIBezierPath也可以

1
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [att length]), path , NULL);

我们可以将 26 这样写

1
2
3
4
5
UIBezierPath * path = [UIBezierPath bezierPathWithRect:self.bounds];
//注意,这里面使用的系统坐标
UIBezierPath * cirlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(10, 200, 100, 100)];
[path appendPath:cirlePath];
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [att length]), path.CGPath, NULL);

7.1

这个时候绘制区域对象已经准备好了,如果没有其他操作的话就可以直接绘制到self.layer上面了

1
CTFrameDraw(frame, context);

7.2

但是有的时候我们需要对不同的行做操作,这个时候我们就要对CTLine分别做绘制了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//获取CTLine数组
CFArrayRef lines = CTFrameGetLines(frame);
//获取CTLine对象数量
CFIndex lineNumber = CFArrayGetCount(lines);
//初始化行原点数组
CGPoint lineOrigins[lineNumber];
//获取每行的原点
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0 ; i < lineNumber; i++) {
//获取行对象
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//设置行的绘制的原点
CGContextSetTextPosition(context, lineOrigins[i].x, lineOrigins[i].y);
//绘制一行的内容
CTLineDraw(line , context);
}

7.3

同时我们有的时候要对每行的CTRun对象做不同的操作

我们将7.2的最后的操作换成下面的代码就是遍历每行的CTRun进行绘制

1
2
3
4
5
CFArrayRef runs = CTLineGetGlyphRuns(line);
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
CTRunDraw(run, context, CFRangeMake(0, 0));
}

8

值得一提的就是,之前向系统申请的Context不需要释放,系统会在恰当的时机去释放

1
2
3
CFRelease(path);
CFRelease(framesetter);
CFRelease(frame);

简单介绍一下概念,具体项目中可以用到的功能持续更新~

随后我们会讲到图文混排、文字点击回调、文字点击高亮等相关操作

Demo

英文水平贼渣的我看了好久英文文档和博客总结的,如果转载请附上链接https://coderwong.com/2018/07/05/CoreText/,万分感谢