很久以前写的文章搬到这里来放着。
iOS开发中经常会遇到做一些文字排版的需求,文字图片混排的需求,在iOS7 以前一般都使用CoreText来处理这样的需求,iOS7之后多了一个TextKit 可以选择,当然TextKit是对CoreText的封装。
CoreText 是用于处理文字和字体的底层技术,它直接和Core Graphics交互;Core Graphics能够直接处理字体和字形,将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。
1.字符(Character)和字形(Glyphs)
排版系统中文本显示的一个重要的过程就是字符到字形的转换,字符是信息本身的元素,而字形是字符的图形表现,字符还会有其它表征比如发音。 字符在计算机中其实就是一个编码,某个字符集中的编码,比如Unicode字符集,就囊括了大多数存在的字符。 而字形则是图形,一般都存储在字体文件中,字形也有它的编码,也就是它在字体中的索引。 一个字符可以对应多个字形(不同的字体,或者同种字体的不同样式:粗体斜体等);多个字符也可能对应一个字形,比如字符的连写( Ligatures)。
Roman Ligatures
下面就来详情看看字形的各个参数也就是所谓的字形度量Glyph Metrics
- bounding box(边界框 bbox),这是一个假想的框子,它尽可能紧密的装入字形。
- baseline(基线),一条假想的线,一行上的字形都以此线作为上下位置的参考,在这条线的左侧存在一个点叫做基线的原点,
- ascent(上行高度)从原点到字体中最高(这里的高深都是以基线为参照线的)的字形的顶部的距离,ascent是一个正值
- descent(下行高度)从原点到字体中最深的字形底部的距离,- descent是一个负值(比如一个字体原点到最深的字形的底部的距离为2,那么descent就为-2)
- linegap(行距),linegap也可以称作leading(其实准确点讲应该叫做External leading),行高lineHeight则可以通过 ascent + |descent| + linegap 来计算。
一些Metrics专业知识还可以参考Free Type的文档 Glyph metrics,其实iOS就是使用Free Type库来进行字体渲染的。
2.坐标系
首先不得不说 苹果编程中的坐标系花样百出,经常让开发者措手不及。 传统的Mac中的坐标系的原点在左下角,比如NSView默认的坐标系,原点就在左下角。但Mac中有些View为了其实现的便捷将原点变换到左上角,像NSTableView的坐标系坐标原点就在左上角。iOS UIKit的UIView的坐标系原点在左上角。
往底层看,Core Graphics的context使用的坐标系的原点是在左下角。而在iOS中的底层界面绘制就是通过Core Graphics进行的,那么坐标系列是如何变换的呢? 在UIView的drawRect方法中我们可以通过UIGraphicsGetCurrentContext()来获得当前的Graphics Context。drawRect方法在被调用前,这个Graphics Context被创建和配置好,你只管使用便是。如果你细心,通过CGContextGetCTM(CGContextRef c)可以看到其返回的值并不是CGAffineTransformIdentity,通过打印出来看到值为
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform transform = CGContextGetCTM(context);
NSLog(@"%@",NSStringFromCGAffineTransform(transform));
[2, 0, 0, -2, 0, 202]//[a=2, b=0, c=0, d=-2, tx=0, ty=202]
Core Text一开始便是定位于桌面的排版系统,使用了传统的原点在左下角的坐标系,所以它在绘制文本的时候都是参照左下角的原点进行绘制的。 但是iOS的UIView的drawRect方法的context被做了次flip,如果你啥也不做处理,直接在这个context上进行Core Text绘制,你会发现文字是镜像且上下颠倒。 如图所示
翻转坐标:
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
3.CoreText排版步骤
使用CoreText进行文字排版的步骤:
1.准备文字也就是 NSMutableAttributedString/ NSAttributedString 对象。
2.根据NSMutableAttributedString创建CTFramesetter并初始化,同时系统自动的创建了CTTypesetter,CTTypesetter就是管理你的字体的类。它作为CTFrame对象的生产工厂,负责根据path生产对应的CTFrame。
3.获取CGPath,用于创建CTFrame。
4.根据CGPath产生对应的CFFrame。
5.使用CTFrameDraw(ctFrame, context);进行绘制文本信息。
CTFrame结构:
CTFrame、CTLine、CTRun三者之间的关系:
CTFrame: 就好比一篇文章,一篇文章会包含多个显示的行
CTLine: 就是上面所说的文章中的每一行,而每一行又包含多个块
CTRun: 就是一行中的很多的块,而块是指 一组共享 相同属性 的字体 的 集合
Code:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//每一个字形都不做图形变换
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//翻转坐标
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//测试文字
NSMutableAttributedString *astring = [[NSMutableAttributedString alloc] initWithString:@"测试富文本显示"];
//设置astring 样式
[astring addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:18] range:NSMakeRange(0, 2)];
[astring addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(2, 2)];
//绘制文本
//初始化ctFramesetter
CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)astring); //创建path
CGMutablePathRef path = CGPathCreateMutable();
CGRect bounds = CGRectMake(0.0, 0.0, self.bounds.size.width, self.bounds.size.height);
CGPathAddRect(path, NULL, bounds); //ctFramesetter 根据path产生ctFrame
CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter,CFRangeMake(0, 0), path, NULL); //绘制ctFrame
CTFrameDraw(ctFrame, context);
}
4.图文混排
图文混排需要Core Text是和Core Graphics配合使用的,一般是在UIView的drawRect方法中的Graphics Context上进行绘制的。 且Core Text真正负责绘制的是文本部分,图片还是需要自己去手动绘制,所以你必须关注很多绘制的细节部分。只是Core Text可以通过CTRun的设置为你的图片在文本绘制的过程中留出适当的空间。这个设置就使用到CTRunDelegate了,看这个名字大概就可以知道什么意思了,CTRunDelegate作为CTRun相关属性或操作扩展的一个入口,使得我们可以对CTRun做一些自定义的行为。为图片留位置的方法就是加入一个空白的CTRun,自定义其ascent,descent,width等参数,使得绘制文本的时候留下空白位置给相应的图片。然后图片在相应的空白位置上使用Core Graphics接口进行绘制。
使用CTRunDelegateCreate可以创建一个CTRunDelegate,它接收两个参数,一个是callbacks结构体,一个是所有callback调用的时候需要传入的对象。 callbacks的结构体为CTRunDelegateCallbacks,主要是包含一些回调函数,比如有返回当前run的ascent,descent,width这些值的回调函数,至于函数中如何鉴别当前是哪个run,可以在CTRunDelegateCreate的第二个参数来达到目的,因为CTRunDelegateCreate的第二个参数会作为每一个回调调用时的入参。
void RunDelegateDeallocCallback( void* refCon )
{
}
CGFloat RunDelegateGetAscentCallback( void *refCon )
{
NSString *imageName = (__bridge NSString *) refCon; return([UIImage imageNamed:imageName].size.height);
}
CGFloat RunDelegateGetDescentCallback( void *refCon )
{
return(0);
}
CGFloat RunDelegateGetWidthCallback( void *refCon )
{
NSString *imageName = (__bridge NSString *) refCon; return([UIImage imageNamed:imageName].size.width);
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//每一个字形都不做图形变换
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//翻转坐标
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//测试文字
NSMutableAttributedString *astring = [[NSMutableAttributedString alloc] initWithString:@"测试富文本显示"];
//设置astring 样式
[astring addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:18] range:NSMakeRange(0, 2)];
[astring addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(2, 2)];
//图片名称
NSString *imgName = @"009@2x.png";
//CTRunDelegateCallbacks用于占位置,给图片预留大小
CTRunDelegateCallbacks imageCallbacks;
imageCallbacks.version = kCTRunDelegateVersion1;
imageCallbacks.dealloc = RunDelegateDeallocCallback;
imageCallbacks.getAscent = RunDelegateGetAscentCallback;
imageCallbacks.getDescent = RunDelegateGetDescentCallback;
imageCallbacks.getWidth = RunDelegateGetWidthCallback;
CTRunDelegateRef ctRunDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void * _Nullable)(imgName));
NSMutableAttributedString *imageAttrString = [[NSMutableAttributedString alloc] initWithString:@" "];
[imageAttrString addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge id)ctRunDelegate range:NSMakeRange(0, 1)];
CFRelease(ctRunDelegate);
//把图片名字与所在的位置绑定,方便后续绘制取出对应图片
[imageAttrString addAttribute:@"imageName" value:imgName range:NSMakeRange(0, 1)];
//图片所在文字的位置
[astring insertAttributedString:imageAttrString atIndex:1];
//绘制文本
CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)astring);
CGMutablePathRef path = CGPathCreateMutable();
CGRect bounds = CGRectMake(0.0, 0.0, self.bounds.size.width, self.bounds.size.height);
CGPathAddRect(path, NULL, bounds);
CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter, CFRangeMake(0, 0), path, NULL);
CTFrameDraw(ctFrame, context);
//绘制图片,遍历找到每一个CTRun 判断是否有图片,然后在绘制出CTRun中对应的图片
CFArrayRef lines = CTFrameGetLines(ctFrame);
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < CFArrayGetCount(lines); i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGFloat lineAscent;
CGFloat lineDescent;
CGFloat lineLeading;
//获取line的绘制矩形
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);
CFArrayRef runs = CTLineGetGlyphRuns(line);
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
CGFloat runAscent;
CGFloat runDescent;
CGPoint lineOrigin = lineOrigins[i];
NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
CGRect runRect;
runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, &runDescent, NULL);
runRect = CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y, runRect.size.width, runAscent+runDescent);
NSString *imageName = [attributes objectForKey:@"imageName"];
if (imageName) {
UIImage *img = [UIImage imageNamed:imageName];
if (img) {
CGRect imageRect;
imageRect.size = img.size;
imageRect.origin.x = lineOrigin.x + runRect.origin.x;
imageRect.origin.y = lineOrigin.y;
CGContextDrawImage(context, imageRect, img.CGImage);
}
}
}
}
CFRelease(ctFrame);
CFRelease(path);
CFRelease(ctFramesetter);
}
参考资料: