CoreText图文混排

我们接着上一篇讲,上篇已经大概的讲了下单纯的绘制文字,但是一篇文章一般都会有几张图片的,而且是嵌入文字内的,不一定是将上下文字换行来处理的,我们今天就讲一下如果图片嵌入了文字该如何处理。

思路

我们已经可以正常绘制富文本了,也可以通过CTLineCTRun来分别对每行或者每行的绘制对象来进行干预来完成绘制。
我们来简单捋一下图文混排的思路,一些准备操作和之前是一样的,只是在将富文本传递给CTFramesetterRef工厂对象时候需要将图片插入富文本中,因为Core Text无法直接绘制图片,只能是将相关的代理传递给一个既定的富文本,之后遍历CTRun来识别再去使用Core Graphics对图片进行绘制在View上面,而属性字符串里面只能存一些高度距离相关的属性。

Demo

图文混排

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

- (void)drawRect:(CGRect)rect {
[super drawRect:rect];

//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.1
NSMutableAttributedString *att = [self getAtt];

//4.2
NSMutableAttributedString *attachment = [self getAttachment:(CGSizeMake(50, 50)) imageName:@"SuperMary"];
[att insertAttributedString:attachment atIndex:3];

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

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

//7.1
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++) {
CGPoint point = lineOrigins[i];
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGContextSetTextPosition(context, point.x, point.y);
CFArrayRef runs = CTLineGetGlyphRuns(line);
CFIndex runNumber = CFArrayGetCount(runs);
for (int j = 0; j < runNumber; j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
//7.2
if ([self isImageAttachment:run]) {
[self drawImage:frame line:line run:run point:point];
}
CTRunDraw(run, context, CFRangeMake(0, 0));
}
}

//8.
CFRelease(path);
CFRelease(frameSetter);
CFRelease(frame);
}
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

- (NSMutableAttributedString *)getAtt{
NSMutableAttributedString *att = [[NSMutableAttributedString alloc] initWithString:@"Core Text, Core Text, Core Text, Core Text, Core Text, Core Text, "];
CTFontRef font = CTFontCreateWithName(CFSTR("PingFang SC"), 24, NULL);
[att addAttribute:(id)kCTFontAttributeName value:(__bridge id)font range:NSMakeRange(0, att.length)];
long number = 10;
CFNumberRef num = CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt8Type,&number);
[att addAttribute:(id)kCTKernAttributeName value:(__bridge id)num range:NSMakeRange(10, 4)];
return att;
}
//1.
- (NSMutableAttributedString *)getAttachment:(CGSize)size imageName:(NSString *)imageName{
CTRunDelegateCallbacks callBacks;
callBacks.version = kCTRunDelegateVersion1;
callBacks.dealloc = DelegateDeallocCallback;
callBacks.getAscent = DelegateAscentCallBacks;
callBacks.getDescent = DelegateDescentCallBacks;
callBacks.getWidth = DelegateWidthCallBacks;

NSString *sizeStr = NSStringFromCGSize(size);

CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks, (__bridge void *)sizeStr);

unichar placeHolder = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
NSMutableAttributedString * imageAttachment = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
[imageAttachment addAttribute:@"imageName" value:imageName range:(NSMakeRange(0, 1))];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imageAttachment, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);

return imageAttachment;
}

//2.
void DelegateDeallocCallback (void *refCon){
NSLog(@"dealloc");
}

CGFloat DelegateAscentCallBacks(void * refCon){
CGSize size = CGSizeFromString((__bridge NSString *)refCon);
return size.height;
}

CGFloat DelegateDescentCallBacks(void * refCon){
return 0;
}

CGFloat DelegateWidthCallBacks(void * refCon){
CGSize size = CGSizeFromString((__bridge NSString *)refCon);
return size.width;
}

//3.
- (BOOL)isImageAttachment:(CTRunRef)run{
NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
NSString *sizeStr = CTRunDelegateGetRefCon(delegate);
if (delegate == nil || ![sizeStr isKindOfClass:[NSString class]]) {
return NO;
}
return YES;
}

//4.
- (void)drawImage:(CTFrameRef)frame line:(CTLineRef)line run:(CTRunRef)run point:(CGPoint)point{

NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);
NSString *imageName = attributes[@"imageName"];
if (!imageName) {
return;
}
CGFloat ascent;
CGFloat descent;
CGRect boundsRun;
boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
boundsRun.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
boundsRun.origin.x = point.x + xOffset;
boundsRun.origin.y = point.y - descent;
CGPathRef path = CTFrameGetPath(frame);
CGRect colRect = CGPathGetBoundingBox(path);
CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);

[self drawWithImageName:imageName rect:imageBounds];
}

//5.
- (void)drawWithImageName:(NSString *)imageName rect:(CGRect)rect{
CGContextRef context = UIGraphicsGetCurrentContext();
UIImage * image = [UIImage imageNamed:imageName];
CGContextDrawImage(context, rect, image.CGImage);
}

代码详解

首先我们可以看到上面一段代码和之前一篇的遍历CTRun的几乎没有区别,只有 4.27.2 的部分有增添,那么我们分别讲一下这两段

1

首先我们先看一下第二段的标号为 1 的代码中

1
2
3
4
5
6
7
8
9
10
11
12
//创建一个CTRun代理回调的结构体
CTRunDelegateCallbacks callBacks;
//设置回调版本,kCTRunDelegateVersion1为默认
callBacks.version = kCTRunDelegateVersion1;
//设置当内存回收时候执行的回调
callBacks.dealloc = DelegateDeallocCallback;
//设置图片距离顶部基线的距离执行的回调
callBacks.getAscent = DelegateAscentCallBacks;
//设置图片距离底部基线的距离执行的回调
callBacks.getDescent = DelegateDescentCallBacks;
//设置图片的宽度执行的回调
callBacks.getWidth = DelegateWidthCallBacks;
1
2
3
4
5
6
7
8
9
10
11
12
13
//创建代理,将参数传到代理对象中
CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks, (__bridge void *)sizeStr);

//创建占位富文本,注意,不要用单空格来进行占位,会导致绘制时候图片位置计算出现问题
unichar placeHolder = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
NSMutableAttributedString * imageAttachment = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
//@"imageName"为自定义的key,将imageName传递到富文本中,在绘制的时候可以直接取用
[imageAttachment addAttribute:@"imageName" value:imageName range:(NSMakeRange(0, 1))];
//给制定范围内的富文本设置代理
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imageAttachment, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
//释放代理对象
CFRelease(delegate);

2

这一段就是给代理回调结构体设置的各个回调,其中返回的形参为

1
2
NSString *sizeStr = NSStringFromCGSize(size);
CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks, (__bridge void *)sizeStr);

中初始化代理时候传入的实参,因为返回时候是个指针,所以所有类型的对象都可以,也可以自定义对象进行传递。

3

我们可以看到 7.2 部分调用了这个方法,这个方法的作用就是判断遍历出来的 CTRun 是否是自己要绘制的富文本,返回一个布尔值。

4

方法的前面的判断就是判断是否有imageName,我们也可以根据需求将一个默认图绘制上去,我们着重讲一下计算图片绝对布局的算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//距离顶部基线的距离
CGFloat ascent;
//距离底部基线的距离
CGFloat descent;
//进行计算的中间变量
CGRect bounds;
//获取图片的宽和上下基线的距离
bounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
bounds.size.height = ascent + descent;
//获取距离行的第一个字的原点的水平距离
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
//计算图片原点的x坐标
bounds.origin.x = point.x + xOffset;
//计算图片原点的y坐标
bounds.origin.y = point.y - descent;
//获取绘制的区域
CGPathRef path = CTFrameGetPath(frame);
//获取裁剪区域的区域
CGRect cutRect = CGPathGetBoundingBox(path);
//获取图片的绝对布局
CGRect drawBounds = CGRectOffset(bounds, cutRect.origin.x, cutRect.origin.y);

5

两个形参分别是需要绘制的图片和图片的绝对布局,我们直接绘制既可。


我将图文混排增加的代码直接写成了各个方法,更灵活更易懂,这个地方我们可以自定义一个类来专门处理图文相关的代理等一系列的参数,YYText就是自定义了一个YYTextRunDelegate这个类来处理相关的,下一篇我们将继续写点击事件相关

Demo

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