Core Text 原理浅谈

iOS 开发中经常会遇到一些文字排版或者图文混排的需求,在 iOS7 以前一般都使用 CoreText 来处理这样的需求,iOS7 之后可以使用系统的 TextKit ,TextKit 是对 CoreText 的封装。
CoreText 是用于处理文字和字体的底层技术,它直接和 Core Graphics 交互;它真正负责绘制的是文本部分,如果要绘制图片,可以使用 CoreText给图片预留出位置,然后用 Core Graphics 绘制。

底层结构图

Core Text 原理浅谈_第1张图片

字形度量

Core Text 原理浅谈_第2张图片
Core Text 原理浅谈_第3张图片

  • 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 对象模型

Core Text 原理浅谈_第4张图片
Core Text 原理浅谈_第5张图片
从模型图中可以看出,我们首先要通过 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);
}

运行效果:
Core Text 原理浅谈_第6张图片
参考文章:

  • 使用CoreText绘制文本
  • CoreText入门知识

你可能感兴趣的:(iOS开发,iOS,Core,Text)