深入理解Core Text排版引擎

iOS系统上可以使用UILable、UITextFileld、TextKit显示文本,TextKit也可以做一些布局控制,但如果需要精细的布局控制,或者自线程异步绘制文本,就必须使用Core Text和Core Graphics,本文比较系统地讲解Core Text排版核心概念。

iOS文本系统框架

iOS文本系统框架

Core Text是iOS 系统文本排版核心框架,TextKit和WebKit都是封装在CoreText上的,TextKit是iOS7引入的,在iOS7之前几乎所有的文本都是 WebKit 来处理的,包括UILable、UITextFileld等,TextKit是从Cocoa文本系统移植到iOS系统的。
文本渲染过程中Core Text只负责排版,具体的绘制操作是通过Core Graphics框架完成的。如果需要精细的排版控制可以使用Core Text,否则可以直接使用Text Kit。

Core Text排版引擎框架

CoreText排版引擎框架

CTFramesetter是Core Text中最上层的类,CTFramesetter持有attributed string并创建CTTypesetter,实际排版由CTTypesetter完成。CTFrame类似于书本中的「页」,CTLine类似「行」,CTRun是一行中具有相同属性的连续字形。CTFrame、CTLine、CTRun都有对应的Draw方法绘制文本,其中CTRun支持最精细的控制。

CoreText排版引擎框架

排版核心概念

要实现精细的排版控制,就必须理解排版的概念,因为Core Text很多api都涉及到排版概念,这些概念是平台无关的,其他系统也一样适应。
排版引擎通过下面两步对文本进行排版:

  • 生成字形(glyph generation)
  • 字形布局(glyph layout)

字符(Characters)和字形(Glyphs)

字符和字形概念比较好理解,下图很直观


Glyphs of the character A

字型(Typefaces)和字体(Fonts)

字型和字体的概念可能没这么好区分,直接引用官方文档的原话

A typeface is a set of visually related shapes for some or all of the characters in a written language.

A font is a series of glyphs depicting the characters in a consistent size, typeface, and typestyle.

字体是字型的子集,字型也叫font family,比如下图:

Fonts in the Times font family

字体属性(Font metrics)

排版引擎要布局字型,就必须知道字型大小和怎样布局,这些信息就叫字体属性,开发过程也是通过这些属性来计算布局的。字体属性由字体设计者提供,同一个字体不同字形的属性相同,主要属性如下图:

Glyph metrics
Glyph metrics
  • baseline:字符基线,baseline是虚拟的线,baseline让尽可能多的字形在baseline上面,CTFrameGetLineOrigins获取的Origins就是每一行第一个CTRun的Origin
  • ascent:字形最高点到baseline的推荐距离
  • descent:字形最低点到baseline的推荐距离
  • leading:行间距,即前一行的descent与下一行的ascent之间的距离
  • advance width:Origin到下一个字形Origin的距离
  • left-side bearing:Origin到字形最左边的距离
  • right-side bearing:字形最右边到下一个字形Origin的距离
  • bounding box:包含字形最小矩形
  • x-height:一般指小写字母x最高的到baseline的推荐距离
  • Cap-height:一般指H或I最高的到baseline的推荐距离

部分字体属性可以通过UIFont的方法获取,ascent、descent、leading可以通过CTRunGetTypographicBounds、CTRunGetTypographicBounds方法获取,通过ascent、descent、leading可以计算line的实际高度。CTFrame、CTLine、CTRun都有提供api获取属性和绘制文本,控制粒度也由高到低,可以根据具体需求使用不同的粒度。
CTLine上不同CTRun默认是底部对齐的,如果一行文本有Attachment,并且Attachment比字体高,会导致字符偏下,如下图:

带有Attachment的文本渲染

如果要使字符居中对齐,可以通过CGContextSetTextPosition调整每个CTRun的originY,调整后如下图:

垂直方向居中

字距调整

排版系统默认按advance width逐个字符渲染,这样会导致有些字符间距离很大,为了使排版后可读性更高,一般会调整字距,如下图:

Kerning

坐标系变换

UIKit和Core Graphics使用不同的坐标系,UIKit坐标系原点在左上角,Core Graphics坐标系在左下角,如下图:

Core Graphics和UIKit坐标系

使用Core Graphics绘制前必须进行坐标变换,否则绘制后的文本是倒立的,
如下图:

坐标未变换

坐标一般通过下面方法进行变换:

//1.设置字形的变换矩阵为不做图形变换
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//2.平移方法,将画布向上平移bounds的高度
CGContextTranslateCTM(context, 0.0f, self.bounds.size.height);
//3.缩放方法,x轴缩放系数为1,则不变,y轴缩放系数为-1,则相当于以x轴为轴旋转180度
CGContextScaleCTM(context, 1.0f, -1.0f);

变换之后就将Core Graphics坐标系变换成UIKit坐标系了。

Attachment

Core Text 不能直接绘制图像,但可以留出空白空间来为图像腾出空间。通过设置 CTRun 的 delegate,可以确定 CTRun 的 ascent space, descent space and width,如下图:

Attachment渲染

当Core Text遇到一个设置了CTRunDelegate的CTRun,它就会询问delegate:“我需要留多少空间给这块的数据”。通过在CTRunDelegate中设置这些属性,您可以在文本中给图片留开空位。具体方法可以参考「 Core Text Tutorial for iOS: Making a Magazine App

点击响应

使用文本渲染的时候经常需要不同的文本响应不同的点击事件,Core Text本身是不支持点击事件的,要实现不同的文本响应不同的点击事件,就必须知道点击的是哪个字符,核心过程:

  • 重写UIView Touch Event方法捕捉点击事件
  • 通过Core Text查找touch point对应的字符

这里主要讲下如何通过Core Text查找touch point对应的字符,核心代码如下:

- (CFIndex)characterIndexAtPoint:(CGPoint)p {
    CFIndex idx = NSNotFound;
    if (!_lines) {
        return idx;
    }
    CGPoint *linesOrigins = (CGPoint*)malloc(sizeof(CGPoint) * CFArrayGetCount(_lines));
    if (!linesOrigins) {
        return idx;
    }
    
    p = CGPointMake(p.x - _rect.origin.x, p.y - _rect.origin.y);
    // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
    p = CGPointMake(p.x, _rect.size.height - p.y);
    
    CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), linesOrigins);
    
    for (CFIndex lineIndex = 0; lineIndex < _fitNumberOfLines; lineIndex++) {
        CGPoint lineOrigin = linesOrigins[lineIndex];
        CTLineRef line = CFArrayGetValueAtIndex(_lines, lineIndex);
        
        // Get bounding information of line
        CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
        CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);
        
        // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter
        CGFloat flushFactor = 0.0;;
        CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, _rect.size.width);
        lineOrigin.x = penOffset;
        lineOrigin.y = lineOrigin.y - _originY;
        
        // Check if we've already passed the line
        if (p.y > yMax) {
            break;
        }
        // Check if the point is within this line vertically
        if (p.y >= yMin) {
            // Check if the point is within this line horizontally
            if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
                // Convert CT coordinates to line-relative coordinates
                CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
                idx = CTLineGetStringIndexForPosition(line, relativePoint);
                break;
            }
        }
    }
    free(linesOrigins);
    return idx;
}

引用

Quartz 2D Programming Guide
Core Text Programming Guide
Text Programming Guide for iOS
Cocoa Text Architecture Guide
Core Text Tutorial for iOS: Making a Magazine App
初识 TextKit
Font wiki

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

推荐阅读更多精彩内容