OC学习37:CoreText(一)CoreText实现图文混排

张建 lol

简介

CoreText 是用于处理文字和字体的底层技术。它直接和 Core Graphics(又被称为Quartz)打交道。Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。如果我们需要将文本内容直接渲染到图形上下文(Graphics context)时,从 性能和实用性来看,最佳方案就是使用coreText

富文本

为什么讲富文本呢,因为了解富文本是学习CoreText的基础,那么什么是富文本呢?简单点来说就是 带有每一个文字属性的字符串,就是富文本,在iOS中有一个专门处理的类 AttributedString

AttributedString 和字符串相似,分为 NSAttributedStringNSMutableAttributedString 两个类,具体内容我就不过多描述,可以百度去学习一下如何使用,so easy,下面我直接上一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化一个富文本对象
NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"这里在测试图文混排,\n我是一个富文本"];
// 添加一个属性属性
[attributeStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:25] range:NSMakeRange(4, 2)];
// 添加多个属性
NSDictionary * dicAdd = @{NSBackgroundColorAttributeName:[UIColor yellowColor],NSLigatureAttributeName:@1};
[attributeStr addAttributes:dicAdd range:NSMakeRange(7, 10)];
// 移除属性
[attributeStr removeAttribute:NSFontAttributeName range:NSMakeRange(32, 9)];
// 赋值给属性文本
UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(100,100,200,40)];
label.numberOfLines = 0;
label.attributedText = attributedStr;

CoreText绘制富文本

CoreText 实现图文混排其实就是在富文本中 插入一个空白的图片占位符 的富文本字符串,通过 代理设值相关的图片尺寸信息,根据富文本得到的frame,计算图片绘制的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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#import "ZJTextImageV.h"
#import<CoreText/CoreText.h>

@interface ZJTextImageV ()

@end
@implementation ZJTextImageV
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor orangeColor];
}
return self;
}

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

// 初始化一个富文本对象
NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"这里在测试图文混排,\n我是一个富文本"];
// 添加文本属性
[attributeStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:25] range:NSMakeRange(4, 2)];

// 插入图片
// 创建一个回调结构体,设值相关参数
CTRunDelegateCallbacks callBacks;
// memset将开辟内存空间初始化
memset(&callBacks, 0, sizeof(CTRunDelegateCallbacks));
// 设置回调版本
callBacks.version = kCTRunDelegateVersion1;
// 设置图片顶部距离基线的距离
callBacks.getAscent = ascentCallBacks;
// 设置图片底部距离基线的距离
callBacks.getDescent = descentCallBacks;
// 设置图片的宽度
callBacks.getWidth = widthCallBacks;
// 创建一个代理
NSDictionary * dicPic = @{@"height":@50,@"width":@100};
CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks,(__bridge void *)dicPic);
// 占位符
unichar placeHolder = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
// 占位富文本
NSMutableAttributedString * placeHolderAttriStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
// 给占位字符串设置代理
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttriStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
// 插入占位字符
[attributeStr insertAttributedString:placeHolderAttriStr atIndex:15];

// 设置行间距
CGFloat lineSpacing = 30;
const CFIndex num = 1;
CTParagraphStyleSetting settings[num] = {{kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing}};//数组
CTParagraphStyleRef paragraphRef = CTParagraphStyleCreate(settings, num);
[attributeStr addAttribute:NSParagraphStyleAttributeName value:(__bridge id)(paragraphRef) range:NSMakeRange(0, attributeStr.length)];
// 释放
CFRelease(paragraphRef);

// 绘制文本
// 一个frame工厂,负责生产frame
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
// 创建绘制区域
CGMutablePathRef path = CGPathCreateMutable();
// 添加绘制尺寸
CGPathAddRect(path, NULL, self.bounds);
// 富文本长度
NSInteger length = attributeStr.length;
// 工厂根据绘制区域及富文本(可选范围,多次设置)设置frame
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);

// 获取当前绘制上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 设值字形的变换矩阵为不做变换
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
// 平移方法,将画布向上平移一个视图高度
CGContextTranslateCTM(context, 0, self.bounds.size.height);
// 缩放方法,x轴缩放系数为1,则不变,y轴缩放系数为-1,则相当于以x轴为轴旋转180度
CGContextScaleCTM(context, 1, -1);
// 整个区域绘制
CTFrameDraw(frame, context);

// 绘制图片
UIImage * image = [UIImage imageNamed:@"icon"];
CGRect imgR = [self calculateImageRectWithFrame:frame];
CGContextDrawImage(context,imgR, image.CGImage);

// 释放
CFRelease(frameSetter);
CFRelease(path);
CFRelease(frame);
}

static CGFloat ascentCallBacks(void * ref)
{
return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue];
}
static CGFloat descentCallBacks(void * ref)
{
return 0;
}
static CGFloat widthCallBacks(void * ref)
{
return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue];
}

-(CGRect)calculateImageRectWithFrame:(CTFrameRef)frame
{
// 根据frame获取需要绘制的线的数组
NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);
// 获取线的数量
NSInteger count = [arrLines count];
// 建立点的数组
CGPoint points[count];
// 获取起点
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
// 遍历线的数组
for (int i = 0; i < count; i ++) {
// 获取线对象
CTLineRef line = (__bridge CTLineRef)arrLines[i];
// 获取run数组
NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);
// 遍历run数组
for (int j = 0; j < arrGlyphRun.count; j ++) {
// 获取run对象
CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];
// 获取run属性
NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);
// 获取代理
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) { // 非空
continue;
}
// 判断代理字典
NSDictionary * dic = CTRunDelegateGetRefCon(delegate);
if (![dic isKindOfClass:[NSDictionary class]]) { // 非空
continue;
}
CGPoint point = points[i]; // 获取一个起点
CGFloat ascent; // 上距
CGFloat descent; // 下距
CGRect boundsRun; // frame
// 获取宽
boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
// 获取高
boundsRun.size.height = ascent + descent;
// 获取x偏移量
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
// point是行起点位置,加上每个字的偏移量得到每个字的x
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);
return imageBounds;
}
}
return CGRectZero;
}

下面进行分段分析

背景介绍

1、首先来看这一段

1
2
3
4
5
6
7
8
9
10
// 获取当前绘制上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 设值字形的变换矩阵为不做变换
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
// 平移方法,将画布向上平移一个视图高度
CGContextTranslateCTM(context, 0, self.bounds.size.height);
// 缩放方法,x轴缩放系数为1,则不变,y轴缩放系数为-1,则相当于以x轴为轴旋转180度
CGContextScaleCTM(context, 1, -1);
// 整个区域绘制
CTFrameDraw(frame, context);
  • 第一句,获取当前绘制上下文
1
CGContextRef context = UIGraphicsGetCurrentContext();

为什么要获取上下文呢,因为我们所有的绘制操作都是在上下文中进行的。

  • 下面三句,是翻转画布的固定写法
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, -1);

CoreText 期初是为OSX设计的,而OSX的坐标圆点在左下角,y轴正正方向朝上,iOS中坐标圆点是左上角,y轴正方向下,所以要进行翻转屏幕,进行坐标转换

2、图片的代理设置

事实上,图文混排就是要插入图片的位置插入一个富文本类型占位符,通过 CTRunDelegate 设置图片。

  • 先设置一个回调的结构体,告诉代理应该回调哪些方法
1
2
3
4
5
6
CTRunDelegateCallbacks callBacks;// 创建一个回调结构体,设置相关参数
memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));// memset开辟内存空间
callBacks.version = kCTRunDelegateVersion1;// 设置回调版本,默认这个
callBacks.getAscent = ascentCallBacks; // 设置图片顶部距离基线的距离
callBacks.getDescent = descentCallBacks; // 设置图片底部距离基线的距离
callBacks.getWidth = widthCallBacks;// 设置图片宽度

什么是顶部、底部基线呢?看下面的图

这是一个 CRun 的尺寸图,CTRun 会以 origin 点作为圆点进行绘制,基线为过圆点的x轴,ascent 即为 CTRun 顶线距基线的距离,descent 即为底线距基线的距离

  • 对应的结构体回调方法
1
2
3
4
5
6
7
8
9
10
11
12
static CGFloat ascentCallBacks(void * ref)
{
return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue];
}
static CGFloat descentCallBacks(void * ref)
{
return 0;
}
static CGFloat widthCallBacks(void * ref)
{
return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue];
}

为什么要设置结构体呢?因为coreText中大量的调用c方法,事实上你会发现大部分跟底层系统有关的方法都需要调c方法,所以设值代理要按照人家的方法来。

  • 创建代理
1
2
NSDictionary * dicPic = @{@"height":@50,@"width":@100};
CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks,(__bridge void *)dicPic);

上面设值了回调结构体,还没告诉代理 图片尺寸,此处 设值代理时绑定了一个返回图片尺寸的字典 ,事实上你可以 绑定任意对象,绑定的对象即是毁掉方法中的参数 ref

3、插入图片

  • 首先创建一个富文本类型的图片占位符,绑定我们的代理
1
2
3
4
5
6
7
8
9
10
// 图片占位符
unichar placeHolder = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
// 占位富文本
NSMutableAttributedString * placeHolderAttriStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
// 给占位字符串设置代理
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttriStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
// 插入占位字符
[attributeStr insertAttributedString:placeHolderAttriStr atIndex:15];
  • Post title:OC学习37:CoreText(一)CoreText实现图文混排
  • Post author:张建
  • Create time:2021-11-26 09:00:29
  • Post link:https://redefine.ohevan.com/2021/11/26/OC/OC学习37:CoreText(一)CoreText实现图文混排/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
On this page
OC学习37:CoreText(一)CoreText实现图文混排