使用CoreText绘制文本

CoreText


CoreText是底层的API,它使用了许多C的函数(例如CTFramesetterCreateWithAttributedStringCTFramesetterCreateFrame)来代替OC的类和方法。
Core Text是和Core Graphics配合使用的,一般是在UIView的drawRect方法中的Graphics Context上进行绘制的。Core Text真正负责绘制的是文本部分,如果要绘制图片,可以使用CoreText给图片预留出位置,然后用Core Graphics绘制。demo地址

CoreText布局会用到attributed strings(CFAttributedStringRef)和graphics paths(CGPathRef)。attributed strings封装了文本的属性,例如字体和颜色;graphics paths定义了文本框的外形。

字形度量

字形度量就是字形的各个参数:

ios_coretext_glyphs_1.jpg
glyph_metrics_2x.png
  • bounding box(边界框),这是一个假想的框子,它尽可能紧密的装入字形。

  • baseline(基线),一条假想的线,一行上的字形都以此线作为上下位置的参考,在这条线的左侧存在一个点叫做基线的原点。

  • ascent(上行高度),从原点到字体中最高(这里的高深都是以基线为参照线的)的字形的顶部的距离,ascent是一个正值。

  • descent(下行高度),从原点到字体中最深的字形底部的距离,descent是一个负值(比如一个字体原点到最深的字形的底部的距离为2,那么descent就为-2)。

  • linegap(行距),linegap也可以称作leading(其实准确点讲应该叫做External leading)。

  • leading,文档说的很含糊,其实是上一行字符的descent到- 下一行的ascent之间的距离。

  • 所以字体的高度是由三部分组成的:leading + ascent + descent。

字形和字符,一些Metrics专业知识还可以参考Free Type的文档 Glyph metrics,其实iOS就是使用Free Type库来进行字体渲染的。苹果文档 Querying Font MetricsText Layout

CoreText对象模型

65cc0af7gw1e2uxd1gmhwj.jpg
65cc0af7gw1e2uyn6r88oj.jpg
core_text_arch_2x.png

运行时的Core Text对象形成一个层次结构,如图1所示。 这个层次结构的顶部是framesetter对象(CTFramesetterRef)。 使用attributed stringgraphics path作为输入,框架设置器会生成一个或多个文本框(CTFrameRef)。 每个CTFrame对象都代表一个段落。

从图中可以看到,我们首先通过CFAttributeString来创建CTFramaeSetter,然后再通过CTFrameSetter来创建CTFrame。
在CTFrame内部,是由多个CTLine来组成的,每个CTLine代表一行,每个CTLine是由多个CTRun来组成,每个CTRun代表一组显示风格一致的文本。

  • CTFrameSetter: CTFrameSetter是通过CFAttributeString进行初始化它负责根据path生产对应的CTFrame;
  • CTFrame:CTFrame可以想象成画布, 画布的大小范围由CGPath决定。CTFrame由很多CTLine组成。 CTFrame可以通过CTFrameDraw函数直接绘制到context上,我们可以在绘制之前,操作CTFrame中的CTline,进行一些参数的微调;
  • CTLine: CTLine可以看做Core Text绘制中的一行的对象,通过它可以获得当前行的line ascent, line descent, line heading,还可以获得CTLine下的所有CTRun;
  • CTRun: CTRun是一组共享相同attributes的集合体;
    要绘制图片,需要用CoreText的CTRun为图片在绘制过程中留出空间。这个设置要用到CTRunDelegate。我们可以在要显示图片的地方,用一个特殊的空白字符代替,用CTRunDelegate为其设置ascent,descent,width等参数,这样在绘制文本的时候就会把图片的位置留出来,用CGContextDrawImage方法直接绘制出来就行了。

创建CTRunDelegate:

CTRunDelegateRef __nullable CTRunDelegateCreate(
    const CTRunDelegateCallbacks* callbacks,
    void * __nullable refCon ) 

创建CTRunDelegate需要两个参数,一个是callbacks结构体,还有一个是callbacks里的函数调用时需要传入的参数。

typedef struct
{
    CFIndex                         version;
    CTRunDelegateDeallocateCallback dealloc;
    CTRunDelegateGetAscentCallback  getAscent;
    CTRunDelegateGetDescentCallback getDescent;
    CTRunDelegateGetWidthCallback   getWidth;
} CTRunDelegateCallbacks;

callbacks是一个结构体,主要包含了返回当前CTRun的ascent,descent和width函数。

代码

自定义一个继承自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);

    NSArray * lines = (NSArray *)CTFrameGetLines(frame);
    NSInteger lineCount = lines.count;
    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];
        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);
            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;
            runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            runBounds.size.height = ascent + descent;
            CFIndex index = CTRunGetStringRange(run).location;
            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);
}

运行后效果如下:

10.22.23.png

异步绘制

上面的drawRect方法是在主线程里调用的,如果绘制的过程比较耗时,可能会阻塞主线程,这时候可以将会值得过程发到子线程里进行,绘制完成后将context转成位图,然后再把位图在主线程里设置到view的layer里。

- (void)drawRect:(CGRect)rect{
    [super drawRect:rect];
   //将绘制过程放入到后台线程中
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
        UIGraphicsBeginImageContext(rect.size);
        //得到当前绘制画布的上下文,用于将后续内容绘制在画布上
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        //将坐标系上下翻转。对于底层的绘制引擎来说,屏幕的左下角是坐标原点(0,0),而对于上层的UIKit来说,屏幕的左上角是坐标原点,为了之后的坐标系按UIKit来做,在这里做了坐标系的上下翻转,这样底层和上层的(0,0)坐标就是重合的了
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, rect.size.height);
        CGContextScaleCTM(context, 1.0,-1.0);
        
        //创建绘制的区域,这里将UIView的bounds作为绘制区域
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, rect);
        
        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);
        
        NSArray * lines = (NSArray *)CTFrameGetLines(frame);
        NSInteger lineCount = lines.count;
        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];
            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);
                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;
                runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
                runBounds.size.height = ascent + descent;
                CFIndex index = CTRunGetStringRange(run).location;
                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);
            }
        }
        
        CGImageRef imageRef = CGBitmapContextCreateImage(context);
        UIImage *image;
        if (imageRef) {
            image = [UIImage imageWithCGImage:imageRef];
            CGImageRelease(imageRef);
        }
        
        UIGraphicsEndImageContext();
       //在主线程中更新
        dispatch_async(dispatch_get_main_queue(), ^{
            self.layer.contents = (__bridge id _Nullable)(image.CGImage);
        });
    });
}

NSAttributedString

看yykit的demo里,微博的页面的富文本使用了NSAttributedString,这里记录下学习笔记。
这里要把"我家这个好忠犬啊~[喵喵] http://t.cn/Ry4UXdF //@我是呆毛芳子蜀黍w:这是什么鬼?[喵喵] //@清新可口喵酱圆脸星人是扭蛋狂魔:窝家这个超委婉的拒绝了窝"在手机上显示成;

![Uploading 屏幕快照 2017-08-17 上午11.11.20_514440.png . . .]

@用户名用到了正则匹配,可以得到一个nsrange的数组,是@用户名的nsrange;
表情也是用到了正则匹配,得到每个表情的nsrange,从本地寻找表情对应的图片,然后用到了NSTextAttachment来生成NSAttributedString,然后把表情进行了替换。
http://t.cn/Ry4UXdF这个链接被替换成了图片和文字,图片是从网络上下载的。可以先判断本地是否有图片的缓存,如果没有,先用占位图生成NSTextAttachment,先显示占位图,等图片下载完以后就重新替换掉图片。
demo

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

推荐阅读更多精彩内容

  • 1.iOS中的round、ceil、floor函数略解 round如果参数是小数,则求本身的四舍五入.ceil如果...
    K_Gopher阅读 1,182评论 1 0
  • 系列文章: CoreText实现图文混排 CoreText实现图文混排之点击事件 CoreText实现图文混排之文...
    老司机Wicky阅读 40,102评论 221 432
  • CoreText是一个进阶的比较底层的布局文本和处理字体的技术,CoreText API在OS X v10.5 和...
    smalldu阅读 13,399评论 18 129
  • 很久以前写的文章搬到这里来放着。iOS开发中经常会遇到做一些文字排版的需求,文字图片混排的需求,在iOS7 以前一...
    AlienJunX阅读 600评论 0 2
  • 本文所涉及的代码你可以在这里下载到https://github.com/kejinlu/CTTest,包含两个项目...
    eb99d15a673d阅读 1,249评论 0 6