iOS 开发中经常会遇到一些文字排版或者图文混排的需求,在 iOS7 以前一般都使用 CoreText 来处理这样的需求,iOS7 之后可以使用系统的 TextKit ,TextKit 是对 CoreText 的封装。
CoreText 是用于处理文字和字体的底层技术,它直接和 Core Graphics 交互;它真正负责绘制的是文本部分,如果要绘制图片,可以使用 CoreText给图片预留出位置,然后用 Core Graphics 绘制。
底层结构图
字形度量
- bounding box(边界框),是一个假想的框子,它尽可能紧密的装入字形。
- baseline(基线),一条假想的线,一行上的字形都以此线作为上下位置的参考,在这条线的左侧存在一个点叫做基线的原点。
- ascent(上行高度),从原点到字体中最高(这里的高深都是以基线为参照线的)的字形的顶部的距离,ascent 是一个正值。
- descent(下行高度),从原点到字体中最深的字形底部的距离,descent 是一个负值(比如一个字体原点到最深的字形的底部的距离为4,那么 descent 就为-4)。
- linegap(行距),linegap 也可以称作 leading(其实准确点讲应该叫做External leading)。
- leading,其实是上一行字符的 descent 到下一行的 ascent 之间的距离。
- 因此字体的高度是由三部分组成的:leading + ascent + descent。
字形和字符 可以参考本文的详解,苹果官方文档:Querying Font Metrics、Text Layout。
CoreText 对象模型
从模型图中可以看出,我们首先要通过 CFAttributeString 来创建 CTFramaeSetter,然后再通过 CTFrameSetter 来创建 CTFrame。
在 CTFrame 内部,是由多个 CTLine 来组成的,每个 CTLine 代表一行,每个 CTLine 是由多个 CTRun 来组成,每个 CTRun 代表一组显示风格一致的文本。
- CTFrameSetter 是通过 CFAttributeString 进行初始化,它负责根据CGPath生成对应的 CTFrame;
- CTFrame 可以通过 CTFrameDraw 函数直接绘制到 context 上,我们可以在绘制之前,操作 CTFrame 中的 CTline,进行一些参数的微调;
- CTLine 可以看做 Core Text 绘制中的一行的对象,通过它可以获得当前行的 line ascent、line descent、line heading,还可以获得 CTLine 下的所有 CTRun;
- CTRun 是一组共享相同 attributes 的集合体。 要绘制图片,需要用CoreText 的 CTRun 为图片在绘制过程中留出空间,这个设置要用到 CTRunDelegate。我们可以在要显示图片的地方,用一个特殊的空白字符代替,用 CTRunDelegate 为其设置 ascent,descent,width 等参数,这样在绘制文本的时候就会把图片的位置留出来,用 CGContextDrawImage 方法直接绘制出来就行了。
创建 CTRunDelegate 需要两个参数,一个是 callbacks 结构体,还有一个是 callbacks 里的函数调用时需要传入的参数。callbacks 是一个结构体,主要包含了返回当前 CTRun 的 ascent,descent 和 width 函数。
typedef struct
{
CFIndex version;
CTRunDelegateDeallocateCallback dealloc;
CTRunDelegateGetAscentCallback getAscent;
CTRunDelegateGetDescentCallback getDescent;
CTRunDelegateGetWidthCallback getWidth;
} CTRunDelegateCallbacks;
Demo示例
自定义一个继承自UIView的子类CoreTextView,在.m文件里引入头文件CoreText/CoreText.h,重写drawRect方法:
void RunDelegateDeallocCallback( void* refCon ){
}
CGFloat RunDelegateGetAscentCallback( void *refCon ){
NSString *imageName = (__bridge NSString *)refCon;
CGFloat height = [UIImage imageNamed:imageName].size.height;
return height;
}
CGFloat RunDelegateGetDescentCallback(void *refCon){
return 0;
}
CGFloat RunDelegateGetWidthCallback(void *refCon){
NSString *imageName = (__bridge NSString *)refCon;
CGFloat width = [UIImage imageNamed:imageName].size.width;
return width;
}
- (void)drawRect:(CGRect)rect{
[super drawRect:rect];
//得到当前绘制画布的上下文,用于将后续内容绘制在画布上
CGContextRef context = UIGraphicsGetCurrentContext();
//将坐标系上下翻转。对于底层的绘制引擎来说,屏幕的左下角是坐标原点(0,0),而对于上层的UIKit来说,屏幕的左上角是坐标原点,为了之后的坐标系按UIKit来做,在这里做了坐标系的上下翻转,这样底层和上层的(0,0)坐标就是重合的了
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0,-1.0);
//创建绘制的区域,这里将UIView的bounds作为绘制区域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
NSMutableAttributedString * attString = [[NSMutableAttributedString alloc] initWithString:@"海洋生物学家在太平洋里发现了一条与众不同的鲸。一般蓝鲸的“歌唱”频率在十五到二十五赫兹,长须鲸子啊二十赫兹左右,而它的频率在五十二赫兹左右。"];
//设置字体
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:24] range:NSMakeRange(0, 5)];
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:13] range:NSMakeRange(6, 2)];
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:38] range:NSMakeRange(8, attString.length - 8)];
//设置文字颜色
[attString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 11)];
[attString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(11, attString.length - 11)];
NSString * imageName = @"jingyu";
CTRunDelegateCallbacks callbacks;
callbacks.version = kCTRunDelegateVersion1;
callbacks.dealloc = RunDelegateDeallocCallback;
callbacks.getAscent = RunDelegateGetAscentCallback;
callbacks.getDescent = RunDelegateGetDescentCallback;
callbacks.getWidth = RunDelegateGetWidthCallback;
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callbacks, (__bridge void * _Nullable)(imageName));
//空格用于给图片留位置
NSMutableAttributedString *imageAttributedString = [[NSMutableAttributedString alloc] initWithString:@" "];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imageAttributedString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
CFRelease(runDelegate);
[imageAttributedString addAttribute:@"imageName" value:imageName range:NSMakeRange(0, 1)];
[attString insertAttributedString:imageAttributedString atIndex:1];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), path, NULL);
//把frame绘制到context里
CTFrameDraw(frame, context);
// 获取CTFrame中所有的line
NSArray * lines = (NSArray *)CTFrameGetLines(frame);
NSInteger lineCount = lines.count;
// 利用CGPoint数组获取所有line的起始坐标
CGPoint lineOrigins[lineCount];
//拷贝frame的line的原点到数组lineOrigins里,如果第二个参数里的length是0,将会从开始的下标拷贝到最后一个line的原点
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < lineCount; i++) {
// 获取每行信息
CTLineRef line = (__bridge CTLineRef)lines[i];
// 得到每行的CTRun信息,并遍历
NSArray * runs = (__bridge NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < runs.count; j++) {
CTRunRef run = (__bridge CTRunRef)runs[j];
NSDictionary * dic = (NSDictionary *)CTRunGetAttributes(run);
// 获取CTRun的代理信息,若无代理信息则直接进入下次循环
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[dic objectForKey:(NSString *)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
NSString * imageName = [dic objectForKey:@"imageName"];
UIImage * image = [UIImage imageNamed:imageName];
CGRect runBounds;
CGFloat ascent;
CGFloat descent;
// 找到CTRunDelegate中的宽度并给上升和下降高度赋值
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CFIndex index = CTRunGetStringRange(run).location;
// 获取CTRun在x上的偏移量
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, index, NULL);
// 起点坐标
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.size =image.size;
CGContextDrawImage(context, runBounds, image.CGImage);
}
}
//底层的Core Foundation对象由于不在ARC的管理下,需要自己维护这些对象的引用计数,最后要释放掉。
CFRelease(frame);
CFRelease(path);
CFRelease(context);
}
运行效果:
参考文章: