CoreText简介(复制一段话吧):
CoreText是用于处理文字和字体的底层技术。它直接和Core Graphics(又被称为Quartz)打交道。Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。
下面是CoreText的架构图,可以看到,CoreText处在非常底层的位置,上层的UI控件(包含UILable、UITextField及UITextView)和UIWebView都是基于CoreText来实现的。
盗一波图吧。。。
UIWebview也是处理复杂的文字排版的备选方案。对于排版,基于CoreText和基于UIWebView相比,具有以下不同点:
- CoreText占用内存更少,渲染速度更快,UIWebView占用内存多,渲染速度慢。
- CoreText在渲染界面前就可以精确地获得显示内容的高度(只要有了CTFrame即可),而UIWebView只有渲染出内容后,才能获得内容的高度(而且还需要通过JavaScript代码来获取)。
- CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
- 基于CoreText可以做更好的原生交互效果,交互效果可以更细腻。而UIWebView的交互效果都是利用JavaScript来实现的,在交互效果上会有一些卡顿情况存在。例如,在UIWebView下,一个简单的按钮按下操作,都无法做出原生按钮的即时和细腻的按下效果。
当然,基于CoreText的排版方案也有那么一些劣势:
- CoreText渲染出来的内容不能像UIWebView那样方便的支付内容的复制。
- 基于CoreText来排版需要自己处理很多复杂逻辑,例如需要自己处理图片和文字混排相关的逻辑,也需要自己实现链接点击操作的支持。
在一次机缘巧合之下了解了CoreText这个东西,看是看了好多资料好多帖子都是模模糊糊的感觉,今天就写点笔记记录一下同时也加深一下印象。
首先呢排版不外乎文字和图片两种格式同事文字和图片再往深的处理也就是文字的显示格式及特殊文字的点击事件和图片的点击事件而已,所以下面就将从最简单的开始由浅入深的介绍一下。
首先介绍几点概念,如果看完感觉模模糊糊的请接着往下看,看完下面的再返回来看会有一种豁然开朗的感觉。
-
CoreText
起初是为OSX设计的,而OSX得坐标原点是左下角,y轴正方向朝上。iOS中坐标原点是左上角,y轴正方向向下。若不进行坐标转换,则文字从下开始,还是倒着的 - 下面要开始一波盗图了
在这你只要知道,一会我们绘制图片的时候实际上是在一个CTRun中绘制这个图片,那么CTRun绘制的坐标系中,他会以origin点作为原点进行绘制。基线为过原点的x轴,ascent即为CTRun顶线距基线的距离,descent即为底线距基线的距离
- 又要盗一波图了
- CTLine 可以看做Core Text绘制中的一行的对象 通过它可以获得当前行的line ascent,line descent ,line leading,还可以获得Line下的所有Glyph Runs
- CTRun 或者叫做 Glyph Run,是一组共享相同attributes(属性)的字形的集合体
一个CTFrame有几个CTLine组成,有几行文字就有几行CTLine。一个CTLine有包含多个CTRun,一个CTRun是所有属性都相同的那部分富文本的绘制单元。所以CTRun是CTFrame的基本绘制单元
纯文字的绘制
1.获取绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext();
2.翻转坐标系
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.view.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
3.获取绘制区域
CGMutablePathRef path = CGPathCreatMutable();
CGPathAddRect(path, NULL, self.bounds);
4.设置绘制内容
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"这仅仅是一段文字"];
5.生成frame
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
CTframeRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attributedString.length), path, NULL);
6.开始绘制
CTFrameDraw(frame, context);
7.释放资源
CFRelease(frame);
CFRelease(frameSetter);
CFRelease(path);
文字中插入图片
1.获取绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext();
2.翻转坐标系
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
3.获取绘制区域
CGMutablePathRef path = CGPathCreatMutable();
CGPathAddRect(path, NULL, self.bounds);
4.设置绘制内容
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"这仅仅是一段文字"];
5.为图片设置回调结构体
//设置一个回调结构体,告诉代理该回调那些方法
CTRunDelegateCallbacks callBacks;//创建一个回调结构体,设置相关参数
memset(&callBacks, 0, sizeof(CTRundelegateCallbacks));//memset将已开辟内存空间 callbacks 的首 n 个字节的值设为值 0, 相当于对CTRunDelegateCallbacks内存空间初始化
callBacks.version = KCTRunDelegateVersion1;//设置回调版本,默认这个
callBacks.getAscent = ascentCallBacks;//设置图片顶部距离基线的距离
callBacks.getDescent = descentCallBacks;//设置图片底部距离基线的距离
callBacks.getWidth = widthCallBacks;//设置图片宽度
6.设置代理
NSDictionary *dict = @{@"width":@100,@"height":@200};//创建一个图片尺寸的字典,初始化代理对象需要
CTRundelegateRef delegate = CTRunDelegateCreate(&callBacks, (__bridge void *)dict);//创建代理
上面只是设置了回调结构体,然而我们还没有告诉这个代理我们要的图片尺寸。所以这句话就在设置代理的时候绑定了一个返回图片尺寸的字典。事实上此处你可以绑定任意对象。此处你绑定的对象既是回调方法中的参数ref
。
将回调方法放到此处
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];
}
7.创建一个富文本类型的占位符绑定我们的代理
unichar placeHolder = 0xFFFC;//创建空白字符
NSString *palceHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
NSMutableAttributedString *placeHolderAttStr = [[NSMutableAttributedString alloc] initWithString:palceHolderStr];
CFAttributedStringSetAttributed((CTAttributedStringRef)placeHolderAttStr, CFRangeMake(0, 1), KCTRunDelegateAttributedName, delegate);//给字符串中的范围字符串设置代理
CFRelease(delegate);//释放代理
8.将占位富文本插入到我们的富文本当中的某个位置
[attributedString insertAttributedSrting:placeHolderAttStr atIndex:3];
9.生成富文本的frame
CTFramesetterRef frameSetter = CTframeSetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedString.length), path, NULL);
10.开始绘制文字
CTFrameDraw(frame, context);
11.根据文本frame找到图片占位符的位置并返回图片绘制区域(这里面好多东西)
思路呢,就是遍历我们的frame中的所有CTRun
,检查他是不是我们绑定图片的那个(依据呢就是我们给占位图片的CTRun
绑定过代理),如果是,根据该CTRun
所在CTLine
的origin
以及CTRun
在CTLine
中的横向偏移量计算出CTRun
的原点,加上其尺寸即为该CTRun
的尺寸。
//---根据frame返回图片绘制的区域---
-(CGRect)handleActiveRectWithFrame:(CTFrameRef)frame {
NSArray *arrLines = (NSArray *)CTFrameGetLines(frame); //根据frame获取CTLine数组
NSInteger count = [arrLines count]; //线的数量
CGPoint points[count]; //创建一个起点数组(CGPoint为结构体,故用C语言数组)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points); //获取起点
for (int i = 0; i < count; i++) { //遍历线的数组
CTLineRef line = (__bridge CTLineRef)arrLines[i];
NSArray *arrRuns = (__bridge CTRunRef)CTLineGetGlyphRuns(line);
for (int j = 0; j < arrRuns.count; j++) { //遍历CTRun数组
CTRunRef run = (__bridge CTRunRef)arrRuns[j];
NSDictionary *dictionary = (NSDictionary *)CTRunGetAttributes(run); //获取CTRun的属性
CTRunDekegateRef delegate = (__bridge CTRunDelegateRef)[dictionary valueForKey:(id)KCTRunDelegateAttributeName]; //获取CTRun绑定的代理
CGPoint point = points[i]; //获取一个起点
if (delegate == nil) {
continue;
}
NSDictionary *dic = CTRunDelegateGetRefCon(delegate); //获取CTRunDelegate绑定的属性
if (![dic iskindOfClass:[NSDictionary class]]) {
continue;
}
return [self getLocWithFrame:frame CTLine:line CTRun:run origin:point];
}
}
return CGRectZero;
}
-(CGRect)getLocWithFrame:(CTFrameRef)frame CTLine:(CTLineRef)line CTRun:(CTRunRef)run origin:(CGPoint)origin {
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);
bounds.origin.x = origin.x + xOffset; ///point是行起点位置,加上每个字的偏移量得到每个字的x
bounds.origin.y = origin.y - descent;
CGPathRef path = CTFrameGetPath(frame); //获取绘制区域
CGRect colRect = CGPathGetBoundingBox(path); //获取裁剪区域边框
CGRect deleteBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y); //获取绘制区域
return deleteBounds;
}
12.开始绘制图片
UIImage *image = [UIImage imageNamed:@"test_image"];
CGRect imgRect = [self handleActiveRectWithFrame:frame];
CGContextDrawImage(context, imgRect, image.CGimage);
13.释放资源
CFRelease(frame);
CFRelease(path);
CFRelease(frameSetter);
参考资料:
iOS:基于CoreText的排版引擎
CoreText实现图文混排