本文的主要内容是如何使用在CoreText绘制的文本内容中添加图片的绘制,实现一个简单的图文混排。此外,因为图文的混排复杂度上会比单纯的文本绘制高一些,涉及到的CoreText的一些概念的API也会在这篇文章有进行详细的讲解,辅助对代码的理解。
其它文章:
CoreText入门(一)-文本绘制
CoreText入门(二)-绘制图片
CoreText进阶(三)-事件处理
CoreText进阶(四)-文字行数限制和显示更多
CoreText进阶(五)- 文字排版样式和效果
CoreText进阶(六)-内容大小计算和自动布局
CoreText进阶(七)-添加自定义View和对其
本文的主要内容
- CoreText框架中重要的类
- CTFrame
- CTLine
- CTRun
- CTRunDelegate
- 绘制图片
- 计算图片位置流程图
- 关键代码
- 一些问题
- CF对象 vs OC对象
- 手动释放内存
Demo:CoreTextDemo
CoreText框架中重要的类
CoreText框架中重要的类示例图

CTFrame
如上图中最外层(蓝色框)的内容区域对应的就是CTFrame,绘制的是一整段的内容,CTFrame有以下几个常用的方法
- CTFrameGetLines 获取CTFrame中包含所有的CTLine
- CTFrameGetLineOrigins 获取CTFrame中每一行的其实坐标,结果保存在返回参数中
- CTFrameDraw 把CTFrame绘制到
CGContext
上下文
CTLine
如上图红色框中的内容就是CTLine,一共有三个CTLine对象,CTLine有以下几个常用的方法
- CTLineGetGlyphRuns 获取CTLine包含的所有的CTRun
- CTLineGetOffsetForStringIndex 获取CTRun的起始位置
CTRun
如上图绿色框中的内容就是CTRun,每一行中相同格式的一块内容是一个CTRun,一行中可以存在多个CTRun,CTRun有以下几个常用的方法
- CTRunGetAttributes 获取CTRun保存的属性,获取到的内容哦是通过
CFAttributedStringSetAttribute
方法设置给图片属性字符串的NSDictionary,key为kCTRunDelegateAttributeName
,值为CTRunDelegateRef
,更具体的内容查看下面讲解 - CTRunGetTypographicBounds 获取CTRun的绘制属性
ascent
、desent
,返回值是CTRun的宽度 - CTRunGetStringRange 获取CTRun字符串的Range
CTRunDelegate
CTRunDelegate和CTRun是紧密联系的,CTFrame初始化的时候需要用到的图片信息是通过CTRunDelegate的callback获得到的,更具体的内容查看下面讲解,CTRunDelegate有以下几个常用的方法
- CTRunDelegateCreate 创建CTRunDelegate对象,需要传递CTRunDelegateCallbacks对象,使用CFAttributedStringSetAttribute方法把CTRunDelegate对象和NSAttributedString对象绑定,在CTFrame初始化的时候回调用CTRunDelegate对象里面CTRunDelegateCallbacks对象的回调方法返回
Ascent
、Descent
、Width
信息
创建CTRunDelegate对象,传递callback和参数的代码:
- (NSAttributedString *)imageAttributeString {
// 1 创建CTRunDelegateCallbacks
CTRunDelegateCallbacks callback;
memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
callback.getAscent = getAscent;
callback.getDescent = getDescent;
callback.getWidth = getWidth;
// 2 创建CTRunDelegateRef
NSDictionary *metaData = @{@"width": @120, @"height": @140};
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData));
// 3 设置占位使用的图片属性字符串
// 参考:https://en.wikipedia.org/wiki/Specials_(Unicode_block) U+FFFC OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.
unichar objectReplacementChar = 0xFFFC;
NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]];
// 4 设置RunDelegate代理
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
CFRelease(runDelegate);
return imagePlaceHolderAttributeString;
}
- CTRunDelegateGetRefCon 获取到CTRunDelegateCreate初始时候设置的元数据,如下代码中的自动变量
metaData
// 2 创建CTRunDelegateRef
NSDictionary *metaData = @{@"width": @120, @"height": @140};
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData));
绘制图片
绘制图片最重要的一个步骤就是计算图片所在的位置,最后是在drawRect
绘制方法中使用CGContextDrawImage
方法进行绘制图片即可
计算图片位置流程图
计算图片位置流程图

效果图

关键代码
创建CTRunDelegate对象,传递callback和参数的代码,创建CTFrame对象的时候会通过CTRunDelegate
中callbak
的几个回调方法getDescent
、getDescent
、getWidth
返回绘制的图片的信息,方法getDescent
、getDescent
、getWidth
中的参数是CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData))
方法中的metaData
参数,特别地,这里的参数需要把所有权交给CF对象,而不能使用简单的桥接,防止ARC模式下的OC对象自动释放,在方法getDescent
、getDescent
、getWidth
访问会出现BAD_ACCESS的错误
- (NSAttributedString *)imageAttributeString {
// 1 创建CTRunDelegateCallbacks
CTRunDelegateCallbacks callback;
memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
callback.getAscent = getAscent;
callback.getDescent = getDescent;
callback.getWidth = getWidth;
// 2 创建CTRunDelegateRef
NSDictionary *metaData = @{@"width": @120, @"height": @140};
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData));
// 3 设置占位使用的图片属性字符串
// 参考:https://en.wikipedia.org/wiki/Specials_(Unicode_block) U+FFFC OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.
unichar objectReplacementChar = 0xFFFC;
NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]];
// 4 设置RunDelegate代理
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
CFRelease(runDelegate);
return imagePlaceHolderAttributeString;
}
// MARK: - CTRunDelegateCallbacks 回调方法
static CGFloat getAscent(void *ref) {
float height = [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
return height;
}
static CGFloat getDescent(void *ref) {
return 0;
}
static CGFloat getWidth(void *ref) {
float width = [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
return width;
}
计算图片所在的位置的代码:
- (void)calculateImagePosition {
int imageIndex = 0;
if (imageIndex >= self.richTextData.images.count) {
return;
}
// CTFrameGetLines获取但CTFrame内容的行数
NSArray *lines = (NSArray *)CTFrameGetLines(self.richTextData.ctFrame);
// CTFrameGetLineOrigins获取每一行的起始点,保存在lineOrigins数组中
CGPoint lineOrigins[lines.count];
CTFrameGetLineOrigins(self.richTextData.ctFrame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < lines.count; i++) {
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < runs.count; j++) {
CTRunRef run = (__bridge CTRunRef)(runs[j]);
NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
if (!attributes) {
continue;
}
// 从属性中获取到创建属性字符串使用CFAttributedStringSetAttribute设置的delegate值
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
if (!delegate) {
continue;
}
// CTRunDelegateGetRefCon方法从delegate中获取使用CTRunDelegateCreate初始时候设置的元数据
NSDictionary *metaData = (NSDictionary *)CTRunDelegateGetRefCon(delegate);
if (!metaData) {
continue;
}
// 找到代理则开始计算图片位置信息
CGFloat ascent;
CGFloat desent;
// 可以直接从metaData获取到图片的宽度和高度信息
CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
// CTLineGetOffsetForStringIndex获取CTRun的起始位置
CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
CGFloat yOffset = lineOrigins[i].y;
// 更新ImageItem对象的位置
ImageItem *imageItem = self.richTextData.images[imageIndex];
imageItem.frame = CGRectMake(xOffset, yOffset, width, ascent + desent);
imageIndex ++;
if (imageIndex >= self.richTextData.images.count) {
return;
}
}
}
}
绘制图片的代码,使用CGContextDrawImage
方法绘制即可,图片的位置信息就是上一步的代码所获得的
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1, -1);
// 使用CTFrame在CGContextRef上下文上绘制
CTFrameDraw(self.data.ctFrame, context);
// 在CGContextRef上下文上绘制图片
for (int i = 0; i < self.data.images.count; i++) {
ImageItem *imageItem = self.data.images[i];
CGContextDrawImage(context, imageItem.frame, [UIImage imageNamed:imageItem.imageName].CGImage);
}
}
一些问题
CF对象 vs OC对象
关于OC对象和CF对象之间的桥接转换的问题可以查看这篇文章上的讲解 OC对象 vs CF对象
这里有个主意的地方是创建CTRunDelegateRef
对象的时候,这里的参数需要把所有权交给CF对象,需要使用__bridge_retained
,而不能使用简单的桥接,防止ARC模式下的OC对象自动释放,在方法getDescent
、getDescent
、getWidth
访问会出现BAD_ACCESS的错误
// 2 创建CTRunDelegateRef
NSDictionary *metaData = @{@"width": @120, @"height": @140};
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData));
手动释放内存
因为CoreText是属于CF的,需要手动管理内存,比如下面创建的临时变量需要使用CFRelease
及时释放内存,否则会有内存溢出的问题
- (CTFrameRef)ctFrameWithAttributeString:(NSAttributedString *)attributeString frame:(CGRect)frame {
// 绘制区域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, (CGRect){{0, 0}, frame.size});
// 使用NSMutableAttributedString创建CTFrame
CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeString);
CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter, CFRangeMake(0, attributeString.length), path, NULL);
CFRelease(ctFramesetter);
CFRelease(path);
return ctFrame;
}