本文的主要内容是如何使用在CoreText绘制的文本内容中添加图片的绘制,实现一个简单的图文混排。此外,由于图文的混排复杂度上会比单纯的文本绘制高一些,涉及到的CoreText的一些概念的API也会在这篇文章有进行详细的讲解,辅助对代码的理解。git
其它文章:
CoreText入门(一)-文本绘制
CoreText入门(二)-绘制图片
CoreText进阶(三)-事件处理
CoreText进阶(四)-文字行数限制和显示更多
CoreText进阶(五)- 文字排版样式和效果
CoreText进阶(六)-内容大小计算和自动布局
CoreText进阶(七)-添加自定义View和对其数组
本文的主要内容框架
Demo:CoreTextDemo布局
CoreText框架中重要的类示例图
 .net
如上图中最外层(蓝色框)的内容区域对应的就是CTFrame,绘制的是一整段的内容,CTFrame有如下几个经常使用的方法代理
CGContext
上下文如上图红色框中的内容就是CTLine,一共有三个CTLine对象,CTLine有如下几个经常使用的方法code
如上图绿色框中的内容就是CTRun,每一行中相同格式的一块内容是一个CTRun,一行中能够存在多个CTRun,CTRun有如下几个经常使用的方法orm
CFAttributedStringSetAttribute
方法设置给图片属性字符串的NSDictionary,key为kCTRunDelegateAttributeName
,值为CTRunDelegateRef
,更具体的内容查看下面讲解ascent
、desent
,返回值是CTRun的宽度CTRunDelegate和CTRun是紧密联系的,CTFrame初始化的时候须要用到的图片信息是经过CTRunDelegate的callback得到到的,更具体的内容查看下面讲解,CTRunDelegate有如下几个经常使用的方法对象
Ascent
、Descent
、Width
信息建立CTRunDelegate对象,传递callback和参数的代码:blog
- (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; }
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); } }
关于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; }