前言

第一篇中谈到了简单的文本分页计算,这是一个效率非常低的方法,如果有对文本进行预分页,再分章节进行多线程计算,应该可以提高效率,直接计算一卷小说的话,速度勉强可以。如果是一本合集(4MB以上)的话,单纯计算就要消耗大量时间和内存,因为需要生成和保存几千个NSTextContainer或者字符串化的NSRange。不过优点在于TextKit和UITextView高度集成,内存消耗不会太大,比起使用CoreText分页,虽然慢了一点,在处理少量文本,这里的少量应该指大小不超过20KB的文档(目测),所以对于一些新闻类应用,它们的文本长度较小,偶尔会有较多的图文混排,TextKit在这里是非常友好的,但既然我写的是一个阅读器,TextKit就完全没有卵用了。

原先打算使用CoreText分页,然后用UITextView来显示文本,但是UITextView有太多的默认设置了,即使使用相同设置的NSAttributedString,然后把所有可见的排版属性统统设置成相同,CoreText分页的结果还是需要调整,才能跟上UITextView中的布局,如果是在阅读时多次调整字体,对齐方式,行间距或段间距,单纯使用TextKit完全跟不上,而使用CoreText做分页的话,也无法配合UITextView的显示效果。

所以我需要写一个排版引擎还有一个文本视图,不过奇怪的是用Reveal分析多看时,它的文本视图是一个UIImageView。下面就总结接下这几天看到的和测试的。

1.系统字体

[UIFont systemFontOfSize:15.0f];

打印返回的UIFont对象,结果如下

font-family: “.HelveticaNeueInterface-Regular”;

font-weight: normal;

font-style: normal;

font-size: 15.00pt

第一项是字体对象的内存地址;

第二项是一个具体的字体名称,如果支持给定文本语言,则全部使用该字体,否则使用该语言默认的字体,并且保留相同的字体样式与段落格式,但这个方法的问题是如果中文中参杂英文,则英文使用.HelveticaNeueInterface-Regula,中文使用STHeitiSC-Light,也就是说可以选择单一字体,也可以分别指定字体,目前不造如何配置;

第三项是字体粗细程度,iOS8中提供了多个建议的配置常量,常见的有regular,medium,bold,black,粗细程度依次增长。在UIFontDescriptor.h中可以看到,我们可以设定UIFontDescriptor对象的UIFontWeightTrait属性(-1.0~1.0)来直接指定,也可以参照建议的配置常量,如下所示,iOS 8.2以上可用


UIKIT_EXTERN const CGFloat UIFontWeightUltraLight NS_AVAILABLE_IOS(8_2);

UIKIT_EXTERN const CGFloat UIFontWeightThin NS_AVAILABLE_IOS(8_2);

UIKIT_EXTERN const CGFloat UIFontWeightLight NS_AVAILABLE_IOS(8_2);

UIKIT_EXTERN const CGFloat UIFontWeightRegular NS_AVAILABLE_IOS(8_2);

UIKIT_EXTERN const CGFloat UIFontWeightMedium NS_AVAILABLE_IOS(8_2);

UIKIT_EXTERN const CGFloat UIFontWeightSemibold NS_AVAILABLE_IOS(8_2);

UIKIT_EXTERN const CGFloat UIFontWeightBold NS_AVAILABLE_IOS(8_2);

UIKIT_EXTERN const CGFloat UIFontWeightHeavy NS_AVAILABLE_IOS(8_2);

UIKIT_EXTERN const CGFloat UIFontWeightBlack NS_AVAILABLE_IOS(8_2);

第四项是字体样式,分成normal(正常体)italic (斜体) oblique(假斜体)三种,不指定样式时,默认使用normal

斜体(italic)是一种简单的字体风格,对每个字母的结构有一些小改动,倾斜且有曲线,同时有更多的装饰性衬线;

假倾斜(oblique)文本则是正常竖直文本的一个倾斜版本。这两种样式应在ppi大于150的显示设备上使用。

查看mac上的FontBook应用,你会发现,一个具体字体的完整名称如下

Helvetica Light

Helvetica Light Oblique

衬线字体(serif)通常在显示器上较少使用,因为它在字体的末端有装饰性延伸,对于ppi较小的显示器,会造成边缘模糊,常用的都是无衬线字体(sans-serif)。

第五项是字体大小,度量单位是点(point–>pt,通常指72分之一英寸或0.35mm,而em是测量字符高度的单位,是包含字符外形的无形边框高度,en是em的一半,下面会再谈到),这个点与UIKit坐标系中的点是一样的,Retina屏幕上对应4个像素,非Retina屏幕上对应一个像素。

2.自定义字体

好了,让我们再打印一个字体,这次使用默认的简体黑体字STHeitiSC-Light

//Returns a font using CSS name matching semantics.

[UIFont fontWithName:@"Heiti SC" size:15]

打印结果如下

font-family: “STHeitiSC-Light”;

font-weight: normal;

font-style: normal;

font-size: 15.00pt

注释中提到会使用CSS的匹配规则返回请求的字体,我们给出的字体名为Heiti SC,返回的字体是STHeitiSC-Light,我们给定font-family名,它会返回家族中的第一个成员,下一个UIFont类方法可以获取所有可用的font-family的名字

// Returns an array of font family names for all installed fonts

+ (NSArray *)familyNames;

由于返回结果较多,这里就不写出来了,然后可以通过另一个类方法,获取一个font-family的所有成员

// Returns an array of font names for the specified family name

+ (NSArray *)fontNamesForFamilyName:(NSString *)familyName;

所以要获取所有可用字体,可以这样如下查询,或者用UITableView实现一个字体浏览器

1
2
3
4
5
for (NSString *name in [UIFont familyNames]) {

NSLog(@"%@",[UIFont fontNamesForFamilyName:name]);

}

3. 字体的组成与各种属性

由于多数文档给出的例子都是英文字体,中文字体需要自己测试,但总体上和英文字体的各种配置相同,下面都会使用来自apple开发文档的例子。

Q:当我们来谈论iPhone上的字体时,我们在谈论什么?

A:首先我们需要先考虑一下字体什么,还有绘图。

Typographical Concepts

(详情参见apple 开发文档:Cocoa Text Architecture Guide 第13页)

Characters(字符) and Glyphs(标记符号)

字符是一个抽象概念,比如一个“武”字,可以有多种写法,用楷体,宋体打印出的或者自己手写的,都表示同一个字,但这些不同的标记符号,都标记了“武”这个抽象概念,当然也可以称其为“字形”,由于没有什么标准的翻译方式,为了不与下面的概念混淆,我们称glyph标记符号

字符A的各种标记符号

glyph_a_2x

 

a concrete form of a character is called a glyph

但是,字符和标记符号间的关系并不是一对一的,可以一对多,也可以多对一,why?

一对多: 比如有有声调的拼音“e”:“é”,它分为两部分 “e”与头顶上的 “´”,由两个标记符号组成

多对一:Ligture即连体字,英文中常见的有两个连续的ff或连续的fl,如图,这种情况下算一个标记符号

romanligatures_2x

 

Typefaces(字样) and Fonts(字型)

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

1.字样(typeface)是一个抽象的总体概念,可理解为一种设计,它是一种字体设计的字符的集合,包括字母、数字、符号、标点符号等等,其中所有的字符在外观上有着相关联的风格,比如Helvetica。

2.类型风格typestyle~实在找不到合适的翻译),是区别不同字样的一种视觉特征,比如roman类型风格是字体右上角的衬线和字体主干比字体中的横线部分粗,斜体的类型风格在于右倾,接近手写风格…等等,一种字样可以有多种相关联的类型风格,也就是说上文提到的font-style也算是类型风格

3.字型(font)是一系列标记符号,它们有一致的尺寸,字样,类型风格。比如上文提到的

Helvetica与Helvetica Light Oblique,它们有相同的字样与不同的类型风格,后者多出一个Oblique。

4.字体系列font family~或许叫字体家族会更亲切点?)是一组有相同字样和不同类型风格的字体的集合。

Fonts in the Times Family

times_font_family_2x

 

UIFont中给出的属性有

// Font attributes

1
2
3
4
5
6
7
8
9
@property(nonatomic,readonly,retain) NSString *familyName;
@property(nonatomic,readonly,retain) NSString *fontName;
@property(nonatomic,readonly) CGFloat pointSize;
@property(nonatomic,readonly) CGFloat ascender;
@property(nonatomic,readonly) CGFloat descender;
@property(nonatomic,readonly) CGFloat capHeight;
@property(nonatomic,readonly) CGFloat xHeight;
@property(nonatomic,readonly) CGFloat lineHeight NS_AVAILABLE_IOS(4_0);
@property(nonatomic,readonly) CGFloat leading;

名词解释

pointSize:按点计算的尺寸,在retina屏幕上它与字体尺寸大小相等

ascender:上伸部分,字母延伸到大写字母高度以上的部分

descender:字母延伸到基线以下的部分,为负数

capHeight:大写字母的高度

xHeight:小写字母x的高度

lineHeight:行高度,等于decender+|acender|+lineGap

leading:最后是leading,我不造该怎么翻译,文档中提到

The leading value represents the spacing between lines of text and is measured (in points) from baseline to baseline.

也就是说,leading是是两行文字之间的两条基线之间的距离

以上提到的属性都是iOS平台的

font metrics

上面的官方配图是cocoa text architecture guide中的,line gap常称为leading,也就是两行文字间的行间距,这与NSParagraphStyle的lineSpacing的意义相同;

而NSFont没有lineHeight这个属性,有些字体的leading总是为0,且ascender+|descender|不总是等于给定字体大小….

+ (NSFont *)fontWithName:(NSString *)fontName size:(CGFloat)fontSize;

该方法产生的字体的lineHeight应该会根据字体不同做出相应的调整;

对于UIFont,leading总是等于lineHeight,这里的leading与NSFont的完全不同,而它们都等于ascender+|descender|,也就是说iOS的字体的不考虑 lineGap,它交给了NSParagraphStyle的lineSpacing;

iOS上使用一个UITextView的LayoutManager获取的行高度总是大于字体的LineHeight,且随字体增大而增大,这个额外的增量本应该是NSParagraphStyle的lineSpacing(默认值为0),但文档中却指明lineSpacing是归LayoutManager管理,而且就算指定了一个lineSpacing,它还是会根据已经对字体计算的出的lineSpacing来判断是否变更。

当然我关注的是iOS,总的来说,就是

iOS上有些字体在渲染时,lineSpacing默认为0,比如系统默认英文字体:

.HelveticaNeueInterface-Regular

它的lineHeight,leading,以及使用layoutManager计算出的实际文字行高,都是相等的,增大lineSpacing会直接增大文字行高,而对lineHeight,leading无影响;

有些字体渲染时,lineSpacing的默认值由layoutManager计算,比如系统默认中文字体:

STHeitiSC-Light

该字体在给定字体大小为15时,默认lineSpacing为0.45;

如果自定义了lineSpacing,当它大于默认值时,使用自定义的值,小于等于默认值时,使用默认值,也就是小于0.45的行间距都无效

leading,line gap,line spacing等在分页中给我造成了巨巨巨巨大的麻烦,下一篇会给出一些例子。

Text Layout(文本布局)

接着讨论一下文本布局,它们大多囊括在NSParagraphStyle与UIFontDescriptor中,但TextKit接管了一部分文本布局的控制。

glyphterms_2x

如上图所示

Text Direction:默认是从上到下,从左往右,但是有些语言或者排版方式不是这样的,这些在TextKit中由LayoutManager管理,且都可以修改。

Left-side bearing:基线原点到第一个标记符号左侧的距离

Right-side bearing:标记符号间的距离

advance width:从基线原点到right-side bearing终点的长度,也就是说每个标记符号的宽度由

Left-side bearing+Right-side bearing+bounding box的宽度决定,由于每个字母宽度不同,文本的排列也是由advance width决定。

尽管如此,如果标记符号适当靠近,如下图,会有更好的阅读效果,这个由kerning(紧排)决定

修改NSAttribuedString的NSKernAttributeName属性可以改变它。

kerning_2x

 

 

对齐方式(alignment)

默认的对齐方式是向左对齐,也就是说启用分行的情况下,从最左边开始填充标记符号,填满后换行,剩余的空白部分保留在右侧;反之亦然,向右对齐会在左侧留下空白;而居中对齐一般只用于标题,让空白均匀分布在左右侧,justified(公平对齐?)同时采用左右对齐,与居中对齐相反,将空白分布在文字间,如下图所示。

alignmentkinds_2x

justified_2x

 

布局的讨论就暂且搁置,最后说一下绘图。

End

TextKit基于CoreText,CoreText又基于CoreGraphics,我们所做的所有操作其实都是在绘图,对于文字处理,其实就是把文字从字符映射为一个图形,只是底层都都由CoreGraphics完成了。TextKit及CoreText能够缓存字体和布局信息,相对于NSStringDrawing有更高的效率。对于少量文本,NSStringDrawing提供了方便的操作,但文本信息较多时,需要使用TextKit或CoreText,而要追求效率或定制排版引擎或者解析自定义的富文本格式时,还是老实点深入CoreText吧~