CoreText介绍.md

介绍CoreText的一些概念

Core Text基础

CoreText 是用于处理文字和字体的底层技术。它直接和 Core Graphics(又被称为 Quartz)打交道。

UIWebView 也是处理复杂的文字排版的备选方案。对于排版,基于 CoreText 和基于 UIWebView 相比,前者有以下好处:

CoreText 占用的内存更少,渲染速度快,UIWebView 占用的内存更多,渲染速度慢。
CoreText 在渲染界面前就可以精确地获得显示内容的高度(只要有了 CTFrame 即可),而 UIWebView 只有渲染出内容后,才能获得内容的高度(而且还需要用 javascript 代码来获取)
CoreText 的 CTFrame 可以在后台线程渲染,UIWebView 的内容只能在主线程(UI 线程)渲染。
基于 CoreText 可以做更好的原生交互效果,交互效果可以更细腻。而 UIWebView 的交互效果都是用 javascript 来实现的,在交互效果上会有一些卡顿存在。例如,在 UIWebView 下,一个简单的按钮按下效果,都无法做到原生按钮的即时和细腻的按下效果。

当然,基于 CoreText 的排版方案也有一些劣势:

CoreText 渲染出来的内容不能像 UIWebView 那样方便地支持内容的复制。
基于 CoreText 来排版需要自己处理很多复杂逻辑,例如需要自己处理图片与文字混排相关的逻辑,也需要自己实现链接点击操作的支持。

以下是Core Text的构成:

  • CTFrame可以想象成画布, 画布的大小范围由CGPath决定
  • CTFrame由很多CTLine组成, CTLine表示为一行
  • CTLine由多个CTRun组成, CTRun相当于一行中的多个块, 但是CTRun不需要你自己创建, 由NSAttributedString的属性决定, 系统自动生成。每个CTRun对应不同属性
  • CTFramesetter是一个工厂, 创建CTFrame, 一个界面上可以有多个CTFrame

字体

  • 字体(Font):是一系列字号、样式和磅值相同的字符(例如:10磅黑体Palatino)。现多被视为字样的同义词
  • 字面(Face):是所有字号的磅值和格式的综合
  • 字体集(Font family):是一组相关字体(例如:Franklin family包括Franklin Gothic、Fran-klinHeavy和Franklin Compressed)
  • 磅值(Weight):用于描述字体粗度。典型的磅值,从最粗到最细,有极细、细、book、中等、半粗、粗、较粗、极粗
  • 样式(Style):字形有三种形式:Roman type是直体;oblique type是斜体;utakuc type是斜体兼曲线(比Roman type更像书法体)。
  • x高度(X height):指小写字母的平均高度(以x为基准)。磅值相同的两字母,x高度越大的字母看起来比x高度小的字母要大
  • Cap高度(Cap height):与x高度相似。指大写字母的平均高度(以C为基准)
  • 下行字母(Descender):例如在字母q中,基线以下的字母部分叫下伸部分
  • 上行字母(Ascender):x高度以上的部分(比如字母b)叫做上伸部分
  • 基线(Baseline):通常在x、v、b、m下的那条线 描边(Stroke):组成字符的线或曲线。可以加粗或改变字符形状

CoreText定义的字体 样式

可以在 CTStringAttributes.h 中找到

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
const CFStringRef kCTCharacterShapeAttributeName;
//字体形状属性 必须是CFNumberRef对象默认为0,非0则对应相应的字符形状定义,如1表示传统字符形状

const CFStringRef kCTFontAttributeName;
//字体属性 必须是CTFont对象

const CFStringRef kCTKernAttributeName;
//字符间隔属性 必须是CFNumberRef对象

const CFStringRef kCTLigatureAttributeName;
//设置是否使用连字属性,设置为0,表示不使用连字属性。标准的英文连字有FI,FL.默认值为1,既是使用标准连字。也就是当搜索到f时候,会把fl当成一个文字。必须是CFNumberRef 默认为1,可取0,1,2

const CFStringRef kCTForegroundColorAttributeName;
//字体颜色属性 必须是CGColor对象,默认为black

const CFStringRef kCTForegroundColorFromContextAttributeName;
//上下文的字体颜色属性 必须为CFBooleanRef 默认为False

const CFStringRef kCTParagraphStyleAttributeName;
//段落样式属性 必须是CTParagraphStyle对象 默认为NIL

const CFStringRef kCTStrokeWidthAttributeName;
//笔画线条宽度 必须是CFNumberRef对象,默为0.0f,标准为3.0f
const CFStringRef kCTStrokeColorAttributeName;
//笔画的颜色属性 必须是CGColorRef 对象,默认为前景色

const CFStringRef kCTSuperscriptAttributeName;
//设置字体的上下标属性 必须是CFNumberRef对象 默认为0,可为-1为下标,1为上标,需要字体支持才行。如排列组合的样式Cn1

const CFStringRef kCTUnderlineColorAttributeName;
//字体下划线颜色属性 必须是CGColorRef对象,默认为前景色

const CFStringRef kCTUnderlineStyleAttributeName;
//字体下划线样式属性 必须是CFNumberRef对象,默为kCTUnderlineStyleNone 可以通过CTUnderlineStypleModifiers 进行修改下划线风格

const CFStringRef kCTVerticalFormsAttributeName;
//文字的字形方向属性 必须是CFBooleanRef 默认为false,false表示水平方向,true表示竖直方向

const CFStringRef kCTGlyphInfoAttributeName;
//字体信息属性 必须是CTGlyphInfo对象

const CFStringRef kCTRunDelegateAttributeName
//CTRun 委托属性 必须是CTRunDelegate对象

CoreText坐标系

从图中可看出CoreText坐标系是以左下角为坐标原点,而我们常使用的UIKit是以左上角为坐标原点,因此在CoreText中的布局完成后需要对其坐标系进行转换,否则直接绘制出现位置反转的镜像情况。

CoreText 图文混排

CoreText实际上并没有相应API直接将一个图片转换为CTRun并进行绘制,它所能做的只是为图片预留相应的空白区域,而真正的绘制则是交由CoreGraphics完成。(像OSX就方便很多,直接将图片打包进NSTextAttachment即可,根本无须操心绘制的事情,所以基于这个想法,M80AttributedLabel的接口和实现也是使用了attachment这么个概念,图片或者UIView都是被当作文字段中的attachment。)

在CoreText中提供了CTRunDelegate这么个Core Foundation类,顾名思义它可以对CTRun进行拓展:AttributedString某个段设置kCTRunDelegateAttributeName属性之后,CoreText使用它生成CTRun是通过当前Delegate的回调来获取自己的ascent,descent和width,而不是根据字体信息。这样就给我们留下了可操作的空间:用一个空白字符作为图片的占位符,设好Delegate,占好位置,然后用CoreGraphics进行图片的绘制。

基本使用步骤

新建一个UIView类,在其drawRect方法中

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
-(void)drawRect:(CGRect)rect{
[super drawRect:rect];

// 1.获取上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 2.转换坐标系
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context , 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// 3.初始化路径 (绘制的区域)
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path , NULL , self.bounds);
// 4.初始化字符串
NSAttributedString* attString = [[NSAttributedString alloc]initWithString:@"Hello CoreText"];
// 5.初始化framesetter
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
// 6. 绘制frame
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]), path , NULL);
// 7. 绘制富文本
CTFrameDraw(frame, context);

// 8. 释放
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);

}

Hello CoreText就会显示在该View上了,这是CoreText的基本使用步骤。

简单的富文本

在上面代码的基础上,为NSAttributeString添加相关的字体信息

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
NSMutableAttributedString* mAttStr = [[NSMutableAttributedString alloc]initWithString:@"Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;"];

//设置字体属性
CTFontRef font = CTFontCreateWithName(CFSTR("Georgia"), 40, NULL);
[mAttStr addAttribute:(id)kCTFontAttributeName value:(__bridge id _Nonnull)(font) range:NSMakeRange(0, 4)];

//设置斜体字
CTFontRef font2 = CTFontCreateWithName((CFStringRef)[UIFont italicSystemFontOfSize:20].fontName, 14, NULL);
[mAttStr addAttribute:(id)kCTFontAttributeName value:(__bridge id _Nonnull)(font2) range:NSMakeRange(6, 4)];

//下划线
[mAttStr addAttribute:(id)kCTUnderlineStyleAttributeName value:(id)[NSNumber numberWithInt:kCTUnderlineStyleDouble] range:NSMakeRange(18, 4)];
//下划线颜色
[mAttStr addAttribute:(id)kCTUnderlineStyleAttributeName value:(id)[NSNumber numberWithInt:kCTUnderlineStyleSingle] range:NSMakeRange(25, 4)];
[mAttStr addAttribute:(id)kCTUnderlineColorAttributeName value:(id)[UIColor redColor].CGColor range:NSMakeRange(25, 4)];

//设置字体间隔
long number = 10;
CFNumberRef num = CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt8Type,&number);
[mAttStr addAttribute:(id)kCTKernAttributeName value:(__bridge id)num range:NSMakeRange(36, 4)];

//设置字体颜色
[mAttStr addAttribute:(id)kCTForegroundColorAttributeName value:(id)[UIColor redColor].CGColor range:NSMakeRange(43, 4)];

//设置空心字
long number2 = 2;
CFNumberRef num2 = CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt8Type,&number2);
[mAttStr addAttribute:(id)kCTStrokeWidthAttributeName value:(__bridge id)num2 range:NSMakeRange(54, 4)];

效果图:

更多的样式

在介绍如何绘制更多样式之前,需要介绍 按Line绘制 CTLineDraw 和按Run绘制 CTRunDraw

CTLineDraw 按行绘制

将上面的

1
CTFrameDraw(frame, context);

替换成:

1
2
3
4
5
6
7
8
9
10
11
12
13
//7.2使用CTLine绘制
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex numOfLines = CFArrayGetCount(lines);
//获取每一行的origin,CoreText的origin是在字形的baseLine处
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0 ; i < numOfLines; i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//设置每一行的位置
CGContextSetTextPosition(context, lineOrigins[i].x, lineOrigins[i].y);
//开始一行的绘制
CTLineDraw(line , context);
}

CTRunDraw 按Run绘制

将上面的

1
CTFrameDraw(frame, context);

替换成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//7.3 使用CTRun绘制
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex numOfLines = CFArrayGetCount(lines);
//获取每一行的origin,CoreText的origin是在字形的baseLine处
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0 ; i < numOfLines; i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//设置每一行的位置
CGContextSetTextPosition(context, lineOrigins[i].x, lineOrigins[i].y);
//开始每个run的绘制
CFArrayRef runs = CTLineGetGlyphRuns(line);
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
CTRunDraw(run, context, CFRangeMake(0, 0));
}
}

最终 CTRunDraw CTLineDraw 和 CTFrameDraw的绘制结果都是相同的。

实现删除线效果

既然我们可以按照一小块CTRun的方式绘制,就可以对一些CTRun做一些自定义绘制

比如在 < CoreText/CTStringArrtibutes.h >中并没有找到删除线的定义,我们可以尝试去实现它。

一、在NSAttributedString中添加删除线属性(NSStrikethroughStyleAttributeName字段定义在NSAttributedString.h中,coreText没有对应的实现)

1
2
//额外的删除线
[mAttStr addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInt:NSUnderlineStyleSingle] range:NSMakeRange(72,18)];

二、在上面按run绘制的基础上判断一下有无删除线属性,有就绘制。

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
//7.3 使用CTRun绘制
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex numOfLines = CFArrayGetCount(lines);
//获取每一行的origin,CoreText的origin是在字形的baseLine处
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0 ; i < numOfLines; i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//设置每一行的位置
CGContextSetTextPosition(context, lineOrigins[i].x, lineOrigins[i].y);
//开始每个run的绘制
CFArrayRef runs = CTLineGetGlyphRuns(line);
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
CTRunDraw(run, context, CFRangeMake(0, 0));

//获取run的样式
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
//判断是否有指定的样式
if(runAttributes[NSStrikethroughStyleAttributeName] != nil){
//绘制样式
[self drawStrikethroughStyleInRun:run attributes:runAttributes context:context];
}
}
}

三、 实现方法 -drawStrikethroughStyleInRun: attributes : context :

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
55
56
/**
画删除线
*/
-(void)drawStrikethroughStyleInRun:(CTRunRef)run attributes: (NSDictionary*)attributes context:(CGContextRef)context {

CFNumberRef styleRef = (__bridge CFNumberRef)(attributes[NSStrikethroughStyleAttributeName]);
NSUnderlineStyle style = NSUnderlineStyleNone;
CFNumberGetValue(styleRef, kCFNumberSInt64Type, &style);
if (style == NSUnderlineStyleNone) return;

// 2.获得画线的宽度
CGFloat lineWidth = 1;
if((style & NSUnderlineStyleThick) == NSUnderlineStyleThick){
lineWidth = 2;
}
CGContextSetLineWidth(context, lineWidth);

// 3.获取画线的起点
CGPoint firstP = CGPointZero;
size_t length = CTRunGetGlyphCount(run);
const CGPoint *firstGP = CTRunGetPositionsPtr(run);
if (!firstGP) {
NSMutableData *tempBuffer = [[NSMutableData alloc] initWithLength:sizeof(CGPoint) * length];
CTRunGetPositions(run, CFRangeMake(0, length), (CGPoint *)tempBuffer.mutableBytes);
firstGP = tempBuffer.mutableBytes;
}
firstP = *firstGP ;

// 4.开始画线
CGContextBeginPath(context);
//4.1 划线的颜色
CGColorRef lineColor = (__bridge CGColorRef)attributes[NSStrikethroughColorAttributeName];
if(lineColor == nil){
CGContextSetStrokeColorWithColor(context, UIColor.blueColor.CGColor);
}
else{
CGContextSetStrokeColorWithColor(context, lineColor);
}
//4.2高度
UIFont* font = attributes[NSFontAttributeName];
if(font == nil){
font = [UIFont systemFontOfSize:UIFont.systemFontSize];
}
CGFloat lineHeight = font.xHeight / 2.0 + firstP.y;
//4.3多行处理
CGPoint pt = CGContextGetTextPosition(context);
lineHeight += pt.y;
//4.5 删除线的长度
float w = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nil , nil, nil);

// 5. 画线
CGContextMoveToPoint(context, pt.x + firstP.x, lineHeight);
CGContextAddLineToPoint(context, pt.x + firstP.x + w, lineHeight);
CGContextStrokePath(context);

}

实现效果:

实现自定义样式-矩形框标注

自定义一个TSRectColor 一个包住文字的矩形框,尝试去实现它。

一、 在 NSAttributedString 中添加自定义属性:

1
2
//自定义的矩形框
[mAttStr addAttribute:@"TSRectColor" value:[UIColor yellowColor] range:NSMakeRange(90, 18)];

二、 修改一下上面的实现删除线的代码,添加实现矩形框。与实现删除线不一样的是,实现矩形框需要在CTRunDraw之前:

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
//7.3 使用CTRun绘制
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex numOfLines = CFArrayGetCount(lines);
//获取每一行的origin,CoreText的origin是在字形的baseLine处
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0 ; i < numOfLines; i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//设置每一行的位置
CGContextSetTextPosition(context, lineOrigins[i].x, lineOrigins[i].y);
//开始每个run的绘制
CFArrayRef runs = CTLineGetGlyphRuns(line);
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
//获取run的样式
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
if(runAttributes[@"TSRectColor"]){
//绘制自定义矩形框 (需要放在CTRunDraw之前 否则绘制的颜色会挡住文字)
[self drawRectColorInRun:run attributes:runAttributes context:context];
}
CTRunDraw(run, context, CFRangeMake(0, 0));
//判断是否有指定的样式
if(runAttributes[NSStrikethroughStyleAttributeName] != nil){
//绘制删除线
[self drawStrikethroughStyleInRun:run attributes:runAttributes context:context];
}

}
}

三、 实现 -drawRectColorInRun: attributes: context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
绘制矩形框
*/
-(void)drawRectColorInRun:(CTRunRef)run attributes: (NSDictionary*)attributes context:(CGContextRef)context {
//1.判断属性是否存在啊
UIColor* color = attributes[@"TSRectColor"];
if(!color)return;
//2.画线的起点
CGPoint firstPoint = [self firstPointInRun:run];
//3.获取run的宽度 ascent descent
CGFloat ascent,descent;
double typographicBounds = CTRunGetTypographicBounds(run , CFRangeMake(0, 0), &ascent, &descent, NULL);

CGPoint textP = CGContextGetTextPosition(context);
//4. 需要填充的区域
CGRect rect = CGRectMake(firstPoint.x + textP.x, textP.y + firstPoint.y - descent, typographicBounds, ascent + descent);
//5. 开始填充颜色
CGContextSetStrokeColorWithColor(context , color.CGColor);
CGContextAddRect(context, rect);
CGContextStrokePath(context);
}

实现效果:

添加超链接

如何识别文字中的超链接 不同场景有不同的处理方式,这里就不处理了。

添加超链接 简单来讲就是重写touchesBegan方法 获取点击的点,再到CTFrameRef中比较

重写touchesBegan方法 根据touch事件获取点point

1
2
3
4
5
6
7
8
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch* touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
bool ifClicked = [self ifClickLink:point];
if (ifClicked) {
NSLog(@"点击了连接");
}
}

判断touch点

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
-(bool)ifClickLink:(CGPoint)point{
if (!CGRectContainsPoint(self.bounds, point)) {
return false;
}
//textFrame 通过CTFrameGetLineOrigins获取所有line的原点
CFArrayRef lines = CTFrameGetLines(self.textFrame);
if(!lines) return false;
CFIndex count = CFArrayGetCount(lines);
CGPoint origins[count];
CTFrameGetLineOrigins(self.textFrame, CFRangeMake(0, 0), origins);
//CoreText坐标系不同翻转一下
CGAffineTransform transform = CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.f, -1.f);

for (int i = 0 ; i < count; i++) {
CGPoint linePoint = origins[i];
CTLineRef line = CFArrayGetValueAtIndex(lines , i);

//获取CTLine的区域
CGFloat ascent = 0.0f;
CGFloat descent = 0.0f;
CGFloat leading = 0.0f;
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;
CGRect flippedLineRect = CGRectMake(linePoint.x, linePoint.y - descent, width, height);
CGRect lineRect = CGRectApplyAffineTransform(flippedLineRect, transform);

//比较point和line 获取点击处于哪个line
if (CGRectContainsPoint(lineRect, point)) {
//line、point 通过CTLineGetStringIndexForPosition获取到点击字符在整段文字中的 index
CFIndex idx = CTLineGetStringIndexForPosition(line, point);
NSLog(@"%ld",idx);
//判断点击的文字是否在链接文字区域
return [self ifClickRangeContainIndex:idx];
}
}

return false;
}

这样就监测到了点击事件,并且知道点击了哪个字符。再做相关超链接的点击逻辑,就添加超链接功能成功了。

以上代码都在 https://github.com/sunyanyan/TSCode/blob/master/CoreText/CoreText/TestView.m

图文混排

CoreText是不直接支持绘制图片的,但是我们可以先在需要显示图片的地方用一个特殊的空白占位符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度,这样绘制文字的时候就会先把图片的位置留出来,再在drawRect方法里面用CGContextDrawImage绘制图片。

在NSMutableString中添加空白字符为图片占位

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
-(void)addImageAttStr{
//图文混排部分

// CTRunDelegateCallbacks:一个用于保存指针的结构体,由CTRun delegate进行回调
CTRunDelegateCallbacks callbacks;
memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;

// 图片信息字典
NSDictionary *imgInfoDic = @{@"width":@100,@"height":@30};
// 设置CTRun的代理
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)imgInfoDic);
// 使用0xFFFC作为空白的占位符
unichar objectReplacementChar = 0xFFFC;
NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);

[self.sampleAttStr insertAttributedString:space atIndex:50];

}

其中用到的CTRun delegate回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static CGFloat ascentCallback(void *ref) {

return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
}

static CGFloat descentCallback(void *ref) {

return 0;
}

static CGFloat widthCallback(void *ref) {

return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
}

然后在CTFrameDraw之后用CGContextDrawImage绘制图片

1
2
3
//8. 绘制图片
UIImage *image = [UIImage imageNamed:@"one.png"];
CGContextDrawImage(context, [self calculateImagePositionInCTFrame:frame], image.CGImage);

用到的calculateImagePositionInCTFrame方法:

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
- (CGRect)calculateImagePositionInCTFrame:(CTFrameRef)ctFrame {

// 获得CTLine数组
NSArray *lines = (NSArray *)CTFrameGetLines(ctFrame);
NSInteger lineCount = [lines count];
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins);

// 遍历每个CTLine
for (NSInteger i = 0 ; i < lineCount; i++) {

CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);

// 遍历每个CTLine中的CTRun
for (id runObj in runObjArray) {

CTRunRef run = (__bridge CTRunRef)runObj;
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}

NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
if (![metaDic isKindOfClass:[NSDictionary class]]) {
continue;
}

CGRect runBounds;
CGFloat ascent;
CGFloat descent;

runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;

CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent;

CGPathRef pathRef = CTFrameGetPath(ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);

CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
return delegateBounds;
}
}
return CGRectZero;
}

如图: