CoreText图文混排

最近在网上看了一些大牛的文章,自己也试着写了一下,感觉图文混排真的很强大。

废话不多说,开始整

先上效果图跟代码,然后一步步一句句给你分析


这个图是我的APP上面做的一个示范,后面内容不重要,图文混排的部分是红色背景区域

代码

#import "coretext.h"#import@implementation coretext

-(void)drawRect:(CGRect)rect

{

[super drawRect:rect];

CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSetTextMatrix(context, CGAffineTransformIdentity);    CGContextTranslateCTM(context, 0, self.bounds.size.height);

CGContextScaleCTM(context, 1.0, -1.0);

NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\n这里在测试图文混排,\n我是一个富文本"];

CTRunDelegateCallbacks callBacks;

memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));

callBacks.version = kCTRunDelegateVersion1;

callBacks.getAscent = ascentCallBacks;

callBacks.getDescent = descentCallBacks;

callBacks.getWidth = widthCallBacks;

NSDictionary * dicPic = @{@"height":@50,@"width":@50};

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

unichar placeHolder = 0xFFFC;

NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];

NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];

CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);

CFRelease(delegate);

[attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];

CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);

CGMutablePathRef path = CGPathCreateMutable();

CGPathAddRect(path, NULL, self.bounds);

NSInteger length = attributeStr.length;

CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);

CTFrameDraw(frame, context);

UIImage * image = [UIImage imageNamed:@"默认头像"];

CGRect imgFrm = [self calculateImageRectWithFrame:frame];

CGContextDrawImage(context,imgFrm, image.CGImage);

CFRelease(frame);

CFRelease(path);

CFRelease(frameSetter);

}

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

{

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];

NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);

for (int j = 0; j < arrGlyphRun.count; j ++) {

CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];

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;

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);

return imageBounds;

}

}

return CGRectZero;

}

@end

原理分析:

1.你需要弄清楚CoreText的坐标系跟系统坐标系,他们两者是反的,如果你不变换坐标系的话,你会发现你的问题跟图片都是反的

2.CoreText实现图文混排其实就是在富文本中插入一个空白的图片占位符的富文本字符串,通过代理设置相关的图片尺寸信息,根据从富文本得到的frame计算图片绘制的frame再绘制图片这么一个过程。

代码分析

1.翻转坐标系

CGContextRef context =UIGraphicsGetCurrentContext();//获取当前上下文

CGContextSetTextMatrix(context, CGAffineTransformIdentity);//设置字形的变换矩阵为不做图形变换

CGContextTranslateCTM(context, 0, self.bounds.size.height);//平移方法,将画布向上平移一个屏幕高

CGContextScaleCTM(context, 1.0, -1.0);//缩放方法,x轴缩放系数为1,则不变,y轴缩放系数为-1,则相当于以x轴为轴旋转180度

因为coreText使用的是系统坐标,然而我们平时所接触的iOS的都是屏幕坐标,所以要将屏幕坐标系转换系统坐标系,这样才能与我们想想的坐标互相对应。

事实上呢,这三句是翻转画布的固定写法,这三句你以后会经常看到的。

2.设置富文本

NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\n这里在测试图文混排,\n我是一个富文本"];//这句不用我多说吧,最起码得有个富文本啊才能插入不是。

3.图片设置代理

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

/*

设置一个回调结构体,告诉代理该回调那些方法

*/

CTRunDelegateCallbacks callBacks;//创建一个回调结构体,设置相关参数

memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));//memset将已开辟内存空间 callbacks 的首 n 个字节的值设为值 0, 相当于对CTRunDelegateCallbacks内存空间初始化

callBacks.version = kCTRunDelegateVersion1;//设置回调版本,默认这个

callBacks.getAscent = ascentCallBacks;//设置图片顶部距离基线的距离

callBacks.getDescent = descentCallBacks;//设置图片底部距离基线的距离

callBacks.getWidth = widthCallBacks;//设置图片宽度

4.设置图片尺寸

上边只是设置了图片的代理还没有设置图片的尺寸,这里还需要一个知识点

正在上传...取消

这呢就是一个CTRun的尺寸图

一会我们绘制图片的时候实际上实在一个CTRun中绘制这个图片,那么CTRun绘制的坐标系中,他会以origin点作为原点进行绘制。

基线为过原点的x轴,ascent即为CTRun顶线距基线的距离,descent即为底线距基线的距离。

我们绘制图片应该从原点开始绘制,图片的高度及宽度及CTRun的高度及宽度,我们通过代理设置CTRun的尺寸间接设置图片的尺寸。

/*

创建一个代理

*/

NSDictionary * dicPic = @{@"height":@50,@"width":@50};//创建一个图片尺寸的字典,初始化代理对象需要

CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);//创建代理

上面只是设置了回调结构体,然而我们还没有告诉这个代理我们要的图片尺寸。

所以这句话就在设置代理的时候绑定了一个返回图片尺寸的字典。

事实上此处你可以绑定任意对象。此处你绑定的对象既是回调方法中的参数ref。

5.图片的插入

首先创建一个富文本类型的图片占位符,绑定我们的代理

unichar placeHolder = 0xFFFC;//创建空白字符

NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];//已空白字符生成字符串

NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];//用字符串初始化占位符的富文本

CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);//给字符串中的范围中字符串设置代理

CFRelease(delegate);//释放(__bridge进行C与OC数据类型的转换,C为非ARC,需要手动管理)

然后将占位符插入到我们的富文本中

[attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];//将占位符插入原富文本

6.绘制

绘制分成两部分,绘制文本和绘制图片。

为什么要分两部分呢?

因为富文本中你添加的图片只是一个带有图片尺寸的空白占位符啊,你绘制的时候他只会绘制出相应尺寸的空白占位符,所以什么也显示不了啊。

那怎么显示图片啊?拿到占位符的坐标,在占位符的地方绘制相应大小的图片就好了。

6.1绘制文本

CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);//一个frame的工厂,负责生成frame

CGMutablePathRef path = CGPathCreateMutable();//创建绘制区域

CGPathAddRect(path, NULL, self.bounds);//添加绘制尺寸

NSInteger length = attributeStr.length;

CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,length), path, NULL);//工厂根据绘制区域及富文本(可选范围,多次设置)设置frame

CTFrameDraw(frame, context);//根据frame绘制文字

frameSetter是根据富文本生成的一个frame生成的工厂,你可以通过framesetter以及你想要绘制的富文本的范围获取该CTRun的frame。

但是你需要注意的是,获取的frame是仅绘制你所需要的那部分富文本的frame。即当前情况下,你绘制范围定为(10,1),那么你得到的尺寸是只绘制(10,1)的尺寸,他应该从屏幕左上角开始(因为你改变了坐标系),而不是当你绘制全部富文本时他该在的位置。

然后建立一会绘制的尺寸,实际上就是在指定你的绘制范围。

接着生成整个富文本绘制所需要的frame。因为范围是全部文本,所以获取的frame即为全部文本的frame(此处老司机希望你一定要搞清楚全部与指定范围获取的frame他们都是从左上角开始的,否则你会进入一个奇怪的误区,稍后会提到的)。

最后,根据你获得的frame,绘制全部富文本。

6.2绘制图片

上面你已经绘制出文字,不过没有图片哦,接下来绘制图片。

绘制图片用下面这个方法,通用的哦

CGContextDrawImage(context,imgFrm, image.CGImage);//绘制图片

我们可以看到这个方法有三个参数,分别是context,frame,以及image。

要什么就给他什么好咯,context和image都好说,context就是当前的上下文,最开始获得那个。image就是你要添加的那个图片,不过是CGImage类型。通过UIImage转出CGImage就好了,我们重点讲一下frame的获取。

frame的获取

记得我之前说的误区么?这里我们要获得Image的frame,你有没有想过我们的frameSetter?

我也想过,不过就像我说的,你单独用frameSetter求出的image的frame是不正确的,那是只绘制image而得的坐标,所以哪种方法不能用哦,要用下面的方法。

你们一定发现,我获取frame的方法单独写了一个方法,为什么呢?

1.将代码分离,方便修改。

2.最主要的是这部分代码到哪里都能用,达到复用效果。

NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);//根据frame获取需要绘制的线的数组

NSInteger count = [arrLines count];//获取线的数量

CGPoint points[count];//建立起点的数组(cgpoint类型为结构体,故用C语言的数组)

CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);//获取起点

第一句呢,获取绘制frame中的所有CTLine。CTLine,又不知道了吧,老司机又要无耻的盗图了。

CTFrame组成

上面呢,我们能看到一个CTFrame绘制的原理。

CTLine 可以看做Core Text绘制中的一行的对象 通过它可以获得当前行的line ascent,line descent ,line leading,还可以获得Line下的所有Glyph Runs

CTRun 或者叫做 Glyph Run,是一组共享想相同attributes(属性)的字形的集合体

一个CTFrame有几个CTLine组成,有几行文字就有几行CTLine。一个CTLine有包含多个CTRun,一个CTRun是所有属性都相同的那部分富文本的绘制单元。所以CTRun是CTFrame的基本绘制单元。

接着说我们的代码。

为什么我获取的数组需要进行类型转换呢?因为CTFrameGetLines()返回值是CFArrayRef类型的数据。就是一个c的数组类型吧,暂且先这么理解,所以需要转换。

那为什么不用__bridge呢?记得么,我说过,本身就传地址的数据是不用桥接的。就是这样。

然后获取数组的元素个数。有什么用呢,因为我们要用到每个CTLine的原点坐标进行计算。每个CTLine都有自己的origin。所以要生成一个相同元素个数的数组去盛放origin对象。

然后用CTFrameGetLineOrigins获取所有原点。

到此,我们计算frame的准备工作完成了。才完成准备工作。

计算frame

思路呢,就是遍历我们的frame中的所有CTRun,检查他是不是我们绑定图片的那个,如果是,根据该CTRun所在CTLine的origin以及CTRun在CTLine中的横向偏移量计算出CTRun的原点,加上其尺寸即为该CTRun的尺寸。

跟绕口令是的,不过就是这么个思路。

for (int i = 0; i < count; i ++) {//遍历线的数组

CTLineRef line = (__bridge CTLineRef)arrLines[i];

NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);//获取GlyphRun数组(GlyphRun:高效的字符绘制方案)

for (int j = 0; j < arrGlyphRun.count; j ++) {//遍历CTRun数组

CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];//获取CTRun

NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);//获取CTRun的属性

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;//取得高

CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);//获取x偏移量

boundsRun.origin.x = point.x + xOffset;//point是行起点位置,加上每个字的偏移量得到每个字的x

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;

有了上面的思路这里就很好理解了。

外层for循环呢,是为了取到所有的CTLine。

类型转换什么的我就不多说了,然后通过CTLineGetGlyphRuns获取一个CTLine中的所有CTRun。

里层for循环是检查每个CTRun。

通过CTRunGetAttributes拿到该CTRun的所有属性。

通过kvc取得属性中的代理属性。

接下来判断代理属性是否为空。因为图片的占位符我们是绑定了代理的,而文字没有。以此区分文字和图片。

如果代理不为空,通过CTRunDelegateGetRefCon取得生成代理时绑定的对象。判断类型是否是我们绑定的类型,防止取得我们之前为其他的富文本绑定过代理。

如果两条都符合,ok,这就是我们要的那个CTRun。

开始计算该CTRun的frame吧。

获取原点和获取宽高被。

通过CTRunGetTypographicBounds取得宽,ascent和descent。有了上面的介绍我们应该知道图片的高度就是ascent+descent了吧。

接下来获取原点。

CTLineGetOffsetForStringIndex获取对应CTRun的X偏移量。

取得对应CTLine的原点的Y,减去图片的下边距才是图片的原点,这点应该很好理解。

至此,我们已经获得了图片的frame了。因为只绑定了一个图片,所以直接return就好了,如果多张图片可以继续遍历返回数组。

获取到图片的frame,我们就可以绘制图片了,用上面介绍的方法。

在此我借鉴了老司机Wicky的简书,对我帮助很大,我也通过他的博客做了自己的自我总结

参考文章:

http://www.jianshu.com/p/6db3289fb05d

推荐阅读更多精彩内容