Allen Chiang

iOS程序猿的blog

© 2014. All rights reserved.

Text Kit & Core Text 文字排版

文字排版

通常我们使用UILabel、UITextField、UITextView在iOS上展示一些我们需要的文字。前者用于简单的展示,后两者可以用于接受用户的输入。通常情况下我们用上述3者展示简单的纯文本,如果我们需要展示图文混排或者稍微带一点排版样式的文字时,我们需要使用更底层的一些技术,比如Text Kit 或者 Core Text。

上图展示的iOS7的框架层次结构,可以看到基于Core Text的Text Kit为上述3个常用控件提供了技术支持。 首先我们需要准备一些关于字符的预备知识作为铺垫。

字符和字形(Character & Glyphs)

字符(Character)

字符从本质上讲是一个在特定字符集中定义的数字,比如我们在OS X中广泛使用的Unicode。Unicode标准为每一种我们可能会用到的字符,提供了唯一的数字标识。

字形(Glyphs)

字形是用于展示上述字符的一个图形形状。但是每个字符对应的字形,根据字体的不同、字体粗细/斜体定义的不一,并不是唯一的。 例如下图展示的字符“A”的多种字形 字形A

另外字体对应字形也不总是一对一的,在英文中经常就有连写的写法,如下图展示f+f和f+l连写情况下的字形 ff和fl连写

属性字符串(NSAttributedString)

属性字符串(NSAttributedString)在Core Text中使用广泛,接下来你可能会经常遇到他。他用于管理字符串和相关的属性集(例如:字体、字间距),这些属性可以被用于单个字符也可以是一段连续的字符串。 他使用一个NSDictionary来管理唯一标示的属性名称,你可以为任一范围的字符指定想要的字符属性。 在iOS6及以后,你可以通过attributedText属性为UILabel、UITextView、UITextField指定想要展示的字符串。通过这种方式我们就可以展示一些带格式的文本了。但是也许你会问如果要兼容iOS5或者更早的版本,要怎么做呢?这个我们在后面的Core Text部分会有解答。

以下是常见的4种类型,mutable和toll free bridge

NSAttributedString
NSMutableAttributedString
// Toll Free Bridge
CFAttributedString
CFMutableAttributedString

创建方法

initWithString:
initWithString:attributes:
initWithAttributedString:

同时还支持从RTF或者RTFD格式的rich text直接创建

initWithRTF:documentAttributes:
initWithRTFD:documentAttributes:

另外还支持从HTML数据直接创建

initWithHTML:documentAttributes:
initWithHTML:baseURL:documentAttributes:

读写属性

详细参考NSAttributedStringNSMutableAttributedString 的reference

Text kit

好了接下来重新回到我们之前说的Text Kit 和 Core Text中来。 Text Kit是UIKit framework中定义的一组用于提供高性能的排版、布局和展示文字的类和协议,比如展示特别的字间距、行间距、断行规则。 Text Kit中最主要的类之间的数据流图:

更形象化的展示,如下图所示

如果text在一个text container中展示不完全,那么他就会展示到另外一个text container中(如果有的话)。

代码demo:

NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:string];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
self.textContainer = [[NSTextContainer alloc] initWithSize:self.view.bounds.size];
[layoutManager addTextContainer:self.textContainer];
UITextView* textView = [[UITextView alloc] initWithFrame:self.view.bounds 	textContainer:self.textContainer];
[self.view addSubview:textView];

Core Text

位于Text Kit 更底层的 Core Text framework 是一种用于排版和展示文字的技术,他被设计得高效且易用,速度比已有的ATSUI要快2倍以上。自从Core Text随着OSX 10.5(Leopard)的推出以来,很快就取代了ATSUI的地位。

简单的绘制方法

代码例子如下:

NSAttributedString *attributedString ; // 假设这里已经初始化好了
CATextLayer *textLayer = [[CATextLayer alloc ] init];
textLayer = attributedString;
[self.view.layer addSublayer:textLayer];

这里也就解答了前面提到的如何兼容iOS5下通过NSAttributeString来展示带格式的文本了。CATextLayer支持iOS 3.2及以后的版本,所以如果你要支持更早的版本,那么你可能要自己在stackoverflow一下了,呵呵。

Core Text布局引擎和Font api

接下来我们正式回到Core Text的部分。 Core Text主要由两个部分组成,text布局引擎和font API。

  1. 首先通过已经创建好的CFAttributedStringRef(toll bridge to NSAttributedString),使用下面方法创建一个CTFramesetterRef CTFramesetterRef CTFramesetterCreateWithAttributedString( CFAttributedStringRef string );

  2. 通过CTFramesetterRef 和 指定的文字显示区域 CGPathRef 创建CTFrameRef。 根据上图所示,同一个CFAttributedStringRef 和 CGPathRef 可以产生一个或者多个的CTFrameRef,在此同时framesetter将段落的样式应用到CTFrameRef上,比如对齐方式,tab位置,行间距,缩进,断行方式等。

    CTFrameRef CTFramesetterCreateFrame( CTFramesetterRef framesetter, CFRange stringRange, CGPathRef path, CFDictionaryRef frameAttributes );

  3. 在CTFramesetterRef 生成 CTFrameRef的同时,也唤起了一个typesetter(排字,通常是CTTypesetterRef)。她的作用是将CFAttributedStringRef中的字符转换成对应的字形。

  4. 每个段落中(可以理解CTFrameRef代表一个段落)中包含了多行(CTLineRef)。

  5. 每一行中又包含了多个CTRunRef,是指一行中连续的一段包含同样属性和方向的文字。

自定义图文混排

通常情况下图文混排,还是需要开发者自己做一些事情。至于怎么做,这个就需要用到CTRunDelegate了。

本身这是Core Text提供的一个用于自定义CTRun排版属性的回调方法,这里的排版属性包括字形的宽度,字形的向上高度,向下高度。

字形宽度可以理解,但是什么是向上高度和向下高度呢?这个请看下图:

其实这个也比较好理解,大家刚学英语的时候,一定用过那种英文练习簿吧。那么倒数第二根线就是原点所在的基线了,往上就是向上高度,往下就是向下高度。

我们可以通过以下方法来创建一个CTRunDelegateRef

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

CTRunDelegateCallbacks才是真正定义字形宽度、向上高度和向下高度的结构体,我们看一下他的定义:

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

其中的dealloc、getAscent、getDescent、getWidth几个属性的类型都差不多

typedef void (*CTRunDelegateDeallocateCallback) (
	void* refCon );
typedef CGFloat (*CTRunDelegateGetAscentCallback) (
	void* refCon );
typedef CGFloat (*CTRunDelegateGetDescentCallback) (
	void* refCon );
typedef CGFloat (*CTRunDelegateGetWidthCallback) (
	void* refCon );

c语言中的void* 含义是任意类型的指针,但是我理解这里的参数一般指的是NSAttributeString中当前CTRun对应的属性NSDictionary(如果我理解的不对欢迎指出)

  1. 首先我们定义好自己的CTRunDelegateCallbacks:

     void ACRichTextRunDelegateDeallocCallback(void *refCon) {
     }
		
     CGFloat ACRichTextRunDelegateGetAscentCallback(void *refCon) {
         CFDictionaryRef attributes = (CFDictionaryRef) refCon;
         CFStringRef height = CFDictionaryGetValue(attributes, @ATTRIBUTE_IMG_HEIGHT);
         if (height) {
             double heightF = CFStringGetDoubleValue(height);
             return (float) heightF;
         }  	
         return IMG_DEFAULT_HEIGHT;
     }

     CGFloat ACRichTextRunDelegateGetDescentCallback(void *refCon) {
         return 0;
     }
    
     CGFloat ACRichTextRunDelegateGetWidthCallback(void *refCon) {
         CFDictionaryRef attributes = (CFDictionaryRef) refCon;
         CFStringRef height = CFDictionaryGetValue(attributes, @ATTRIBUTE_IMG_WIDTH);
         if (height) {
             double heightF = CFStringGetDoubleValue(height);
             return (float) heightF;
         }
    
         return IMG_DEFAULT_HEIGHT;
     }
    
     // 创建我们自己的CTRunDelegateCallbacks 结构体
     CTRunDelegateCallbacks imageCallbacks;
     imageCallbacks.version = kCTRunDelegateVersion1;
     imageCallbacks.dealloc = ACRichTextRunDelegateDeallocCallback;
     imageCallbacks.getAscent = ACRichTextRunDelegateGetAscentCallback;
     imageCallbacks.getDescent = ACRichTextRunDelegateGetDescentCallback;
     imageCallbacks.getWidth = ACRichTextRunDelegateGetWidthCallback;
    
  2. 之后我们把CTRunDelegateCallbacks结构体设置到CFAttributedString中,并通过range指定作用的范围

     CFMutableAttributedStringRef text ; // 假设这里已经初始化好
     NSDictionary *imgAttributes = component.attributes;
     CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void *) imgAttributes);
     CFAttributedStringSetAttribute(text, CFRangeMake(position, 1), kCTRunDelegateAttributeName, runDelegate);

     // 把属性设置到
     CFAttributedStringSetAttributes(text, CFRangeMake(position, 1), (__bridge CFDictionaryRef) component.attributes, NO);
		CFRelease(runDelegate);
    

经过上面这么多步骤,其实只是告诉Core Text 有一个地方需要占多大的位置,这样系统就会在指定的地方把空间腾出来,不绘制文字上去。真正的图像绘制其实还是需要我们自己通过Core Graphic来做。

  1. 获取CTRun对应的坐标范围

通过创建CTFrameSetterRef,并且获取嵌套的CTFrameRef,以及包含的CTLineRef,从而最终循环到对应的CTRunRef,再通过以下函数获取当前CTRun坐标范围内的相关属性,比如宽度、向上高度和向下高度。

// 后面3个参数ascent/descent/leading都是输出项
double CTRunGetTypographicBounds (   
	CTRunRef run,    
	CFRange range,   
	CGFloat *ascent,    
	CGFloat *descent,    
	CGFloat *leading 
); 
  1. 自定义绘图

上面获取的只是当前run的属性,真正绘图需要的是相对origin的坐标值,所以在循环CTLine和CTRun的时候,要记录下line和run的origin,并累加起来才是真正相对于坐标原点的偏移量。 有了坐标值后面就是真正的Core Graphic绘图了,限于篇幅不再展开去,具体可以参考Apple的官方文档:

参考文献

comments powered by Disqus