CoreText入门知识

很久以前写的文章搬到这里来放着。
iOS开发中经常会遇到做一些文字排版的需求,文字图片混排的需求,在iOS7 以前一般都使用CoreText来处理这样的需求,iOS7之后多了一个TextKit 可以选择,当然TextKit是对CoreText的封装。

CoreText 是用于处理文字和字体的底层技术,它直接和Core Graphics交互;Core Graphics能够直接处理字体和字形,将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。


A3F7B81C-C370-4268-90E5-C1D1491F48A4.png

1.字符(Character)和字形(Glyphs)

排版系统中文本显示的一个重要的过程就是字符到字形的转换,字符是信息本身的元素,而字形是字符的图形表现,字符还会有其它表征比如发音。 字符在计算机中其实就是一个编码,某个字符集中的编码,比如Unicode字符集,就囊括了大多数存在的字符。 而字形则是图形,一般都存储在字体文件中,字形也有它的编码,也就是它在字体中的索引。 一个字符可以对应多个字形(不同的字体,或者同种字体的不同样式:粗体斜体等);多个字符也可能对应一个字形,比如字符的连写( Ligatures)。


3845188204.gif

Roman Ligatures

下面就来详情看看字形的各个参数也就是所谓的字形度量Glyph Metrics

262666560.gif
3439236923.gif
  • 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绘制,你会发现文字是镜像且上下颠倒。 如图所示


2E20DF98-F59A-41B6-A903-F8154EBBCFA4.png

翻转坐标:

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

3.CoreText排版步骤

4F03378C-2AF3-4D1A-92CF-05B24BAF9322.png

使用CoreText进行文字排版的步骤:

1.准备文字也就是 NSMutableAttributedString/ NSAttributedString 对象。

2.根据NSMutableAttributedString创建CTFramesetter并初始化,同时系统自动的创建了CTTypesetter,CTTypesetter就是管理你的字体的类。它作为CTFrame对象的生产工厂,负责根据path生产对应的CTFrame。

3.获取CGPath,用于创建CTFrame。

4.根据CGPath产生对应的CFFrame。

5.使用CTFrameDraw(ctFrame, context);进行绘制文本信息。
CTFrame结构:

1D425790-3FD5-468B-917B-D1B83697445C.png

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.图文混排


184A51FC-C5EC-4DB0-BE30-2D37A4E0FED0.png

图文混排需要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);
}
542A9C50-D0C1-4885-9770-373635C89AD8.png

参考资料:

http://geeklu.com/2013/03/core-text/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 本文所涉及的代码你可以在这里下载到https://github.com/kejinlu/CTTest,包含两个项目...
    eb99d15a673d阅读 1,245评论 0 6
  • 1.iOS中的round、ceil、floor函数略解 round如果参数是小数,则求本身的四舍五入.ceil如果...
    K_Gopher阅读 1,177评论 1 0
  • CoreText 框架中最常用的几个类 CTFont CTFontCollection CTFontDescrip...
    神采飞扬_2015阅读 1,026评论 1 11
  • blog.csdn.net CoreText实现图文混排 - 博客频道 CoreText实现图文混排 也好久没来写...
    K_Gopher阅读 590评论 0 0
  • iOS没有现成的支持图文混排的控件,而要用多个基础控件组合拼成图文混排这样复杂的排版,是件很苦逼的事情。对此的解决...
    清风沐沐阅读 667评论 0 2