第9章 iOS 7中文字排版和渲染引擎——Text Kit

第 9 章 iOS 7中文字排版和渲染引擎——Text Kit

在iOS 7之前,应用中字体的大小用户是不能设置的,而且开发人员要想实现多种样式的文字排版是件非常麻烦的事情。在iOS 7之后,这些问题都解决了,Text Kit就是解决这些问题的钥匙。本章将向大家介绍iOS 7中文字排版和渲染引擎——Text Kit。

9.1 Text Kit基础

Text Kit最主要的作用就是为程序提供文字排版和渲染的功能。通过Text Kit可以对文字进行存储、布局,以更加精准的排版方式来显示文本内容。Text Kit隶属于UIKit框架,其中包含了一些文字排版的相关类和协议。

9.1.1 文字的排版和渲染

在iOS 7之前也有一种用于文字排版和渲染的技术——Core Text,而引入Text Kit的目的并非要取代Core Text。Core Text是面向底层的文字排版和渲染技术,如果我们需要将文本内容直接渲染到图形上下文时,从性能角度考虑,最佳方案就是使用Core Text。但是从易用性角度考虑,使用Text Kit是最好的选择,因为它能够直接使用UIKit提供的一些文本控件,例如:UITextView、UILabel和UITextField,对文字进行排版。

Text Kit具有很多优点:文本控件UITextView、UITextField和UILabel是构建于Text Kit之上的。Text Kit完全掌控着文字的排版和渲染:可以调整字距、行距、文字大小,指定特定的字体,对文字进行分页或分栏,支持富文本编辑、自定义文字截断,支持文字的换行、折叠和着色等处理,支持凸版印刷效果。

9.1.2 Text Kit架构

在开始介绍Text Kit API之前,我们有必要理解一下iOS的文字渲染框架。从图9-1可见,Text Kit是基于Core Text构建的,它通过Core Text与Core Graphics进行交互。而文本控件,如:UILabel、UITextField和UITextView,则构建于Text Kit之上,可见这些文本控件可以利用Text Kit提供的API来对文字进行排版和渲染处理。从图9-1可见,我们也可以看到UIWebView是基于WebKit的,它不能使用Text Kit提供的功能。

图像说明文字

图9-1 iOS 7之后的文字渲染

图9-2所示是iOS 7之前的文字渲染,可以看出在iOS 7之前没有Text Kit。文本控件,如:UILabel、UITextField和UITextView是基于String Drawing和WebKit构建的。其中String Drawing与Core Graphics直接通信。因此在iOS 7之前文本控件也可以实现多种样式的文字排版,但是事实上是通过WebKit实现的。WebKit是一种浏览器内核技术,使用它进行文字渲染会消耗掉比较多的内存,对应用的性能有一定的影响。

图像说明文字

图9-2 iOS 7之前的文字渲染

9.1.3 Text Kit中的核心类

我们在使用Text Kit时,会涉及如下核心类。

  • NSTextContainer。定义了文本可以排版的区域。默认情况下是矩形区域,如果是其他形状的区域,需要通过子类化NSTextContainer来创建。
  • NSLayoutManager。该类负责对文字进行编辑排版处理,将存储在NSTextStorage中的数据转换为可以在视图控件中显示的文本内容,并把字符编码映射到对应的字形上,然后将字形排版到NSTextContainer定义的区域中。
  • NSTextStorage。主要用来存储文本的字符和相关属性,是NSMutableAttributedString的子类(见图9-3)。此外,当NSTextStorage中的字符或属性发生改变时,会通知NSLayoutManager,进而做到文本内容的显示更新。
  • NSAttributedString。支持渲染不同风格的文本。
  • NSMutableAttributedString。可变类型的NSAttributedString,是NSAttributedString的子类(见图9-3)。

图9-3 NSAttributedString类图

NSLayoutManagerNSTextContainerNSTextStorage之间究竟是什么关系呢?图9-4所示文本控件通过它们实现了显示文本内容到屏幕上的过程。NSLayoutManager对象从NSTextStorage对象中取得文本内容,进行排版,然后把排版之后的文本放到NSTextContainer对象指定的区域上。最后再由一个文本控件从NSTextContainer中取出内容显示到屏幕中。

图像说明文字

图9-4 NSLayoutManager、NSTextContainer和NSTextStorage之间的关系

NSLayoutManager对象起到承上启下的作用。还记得铅字排版吗?在没有计算机排版的时代,排版工人都是通过这种方法实现的,他们从铅字库中找到特定字体的字母,然后把它放到活动字模中(见图9-5),最后进行印刷。这个过程可以很好地帮助我们理解NSLayoutManagerNSTextContainerNSTextStorage之间的关系,其中NSLayoutManager对象相当于排版工人,NSTextStorage对象相当于特定字体的铅字库,而NSTextContainer对象就相当于我们看到的活动字模。文本控件从NSTextContainer中取出内容显示到屏幕的过程就相当于印刷的过程。

图像说明文字

图9-5 铅字排版1

1该图出自维基百科 http://zh.wikipedia.org/wiki/File:MetalTypeZoomIn.JPG

9.1.4 实例:凸版印刷效果

为了更好地理解我们前面介绍的API内容,下面我们通过一个实例介绍NSLayoutManagerNSTextContainerNSTextStorage三者之间的关系。

在Xcode中选择Single View Application模板,创建一个名为TextKit_Sample的工程,在创建时选择Devices为Universal。工程创建成功后,打开Main_iPhone.storyboard故事板文件,从对象库中拖曳TextView控件到设计视图上,并修改其文本内容,如图9-6所示。

图像说明文字

图9-6 拖曳TextView控件

拖曳完成后,要为其定义输出口属性。ViewController.h文件代码如下:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic,strong) NSTextContainer* textContainer;                        ①

@property (strong, nonatomic) IBOutlet UITextView *textView;                         ②

- (void) markWord:(NSString*)word inTextStorage:(NSTextStorage*)textStorage;        ③

@end

上述代码第①行声明了NSTextContainer类型的属性textContainer。代码第②行声明了TextView控件属性。第③行代码声明一个方法,用于设置某些单词样式风格。

ViewController.m文件中viewDidLoad方法代码如下:

- (void)viewDidLoad
{
    [super viewDidLoad];

    CGRect textViewRect = CGRectInset(self.view.bounds, 10.0, 20.0);                   ①

    NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:_textView.text];②

    NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];                    ③

    [textStorage addLayoutManager:layoutManager];                                    ④
    _textContainer = [[NSTextContainer alloc] initWithSize:textViewRect.size];            ⑤
    [layoutManager addTextContainer:_textContainer];                                    ⑥

    [_textView removeFromSuperview];                                                  ⑦
    _textView = [[UITextView alloc] initWithFrame:textViewRect
                                    textContainer:_textContainer];                    ⑧

    [self.view addSubview:_textView];                                                ⑨

    //设置凸版印刷效果
    [textStorage beginEditing];                                                     ⑩
    NSDictionary *attrsDic = @{NSTextEffectAttributeName: NSTextEffectLetterpressStyle};     

    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] 
        initWithString:_textView.text attributes:attrsDic];                              

    [textStorage setAttributedString:attrStr];                                          

    [self markWord:@"我" inTextStorage:textStorage];                                  
    [self markWord:@"I" inTextStorage:textStorage];                                      

    [textStorage endEditing];                                                          

}

上述代码第①行是创建一个矩形区域,这个区域是通过CGRectInset函数创建的,这个函数能够指定一个中心点,后面的两个参数沿着self.view.bounds区域向内缩进量。这样可以使得文字部分不会太靠近视图的边界。

第②行代码是创建NSTextStorage对象,它需要一个字符串作为构造方法的参数,这里我们是从TextView控件取出来赋值给它的。第③行代码是创建NSLayoutManager对象。第④行代码是将刚刚创建的NSTextStorageNSLayoutManager对象关联起来。第⑤行代码是创建NSTextContainer对象,在创建它的时候,通过构造方法设置它的区域,我们这里设置的区域与TextView区域是相同的。第⑥行代码是将刚刚创建的NSLayoutManagerNSTextContainer对象关联起来。

第⑦~⑨行代码是重新构建原来的TextView控件,并且重新添加到视图上。这主要是因为只有重新创建代码才能通过Text Kit中NSLayoutManager来管理,而原来在Interface Builder中创建的TextView控件不再使用了。

第⑩~⑯行代码是实现设置凸版印刷效果,这些设置代码是需要放在[textStorage beginEditing][textStorage endEditing]之间的。第⑪行代码是声明一个字典对象,其中包括@{NSTextEffectAttribute Name:NSTextEffectLetterpressStyle}NSTextEffectAttributeName是文本效果键,而NSTextEffectLetterpressStyle是文本效果值,这里面它们都是常量。第⑫行代码是创建NSMutableAttributedString对象,在构造方法中需要指定要设置的文本以及文本的样式。第⑬行代码是将通过NSTextStorage对象的setAttributedString:方法设置文本NSTextStorage还有类似的方法addAttributedString:,该方法是添加新的设置文本,这样可以叠加多种效果。第⑭~⑮行代码是调用markWord:inTextStorage:方法实现特定单词的查找,并添加设置效果。

ViewController.m文件中的markWord:inTextStorage:方法代码如下:

- (void) markWord:(NSString*)word inTextStorage:(NSTextStorage*)textStorage
{

    NSRegularExpression *regex = [NSRegularExpression 
                                                regularExpressionWithPattern:word
                                            options:0 error:nil];                        ①

    NSArray *matches = [regex matchesInString:_textView.text
                                      options:0
                                        range:NSMakeRange(0, [_textView.text length])];    ②

    for (NSTextCheckingResult *match in matches) {                                    ③
        NSRange matchRange = [match range];                                            
        [textStorage addAttribute:NSForegroundColorAttributeName
                                      value:[UIColor redColor] 
                                      range:matchRange];                                        ④
    }
}

上述代码第①行是创建正则表达式NSRegularExpression对象,其中的regularExpressionWithPattern参数指定正则表达式。第②行代码通过正则表达式NSRegularExpression对象对TextView中的文本内容进行扫描,结果放到数组中。第③行代码从集合中取出NSTextCheckingResult结果对象。第④行代码是为找到的文本设置颜色为红色风格。

编码完成之后我们就可以运行一下看看效果了,如图9-7所示,其中的“我”和“I”是红色显示的,整个的文字设置为凸版印刷效果。

图9-7 运行效果

9.2 文字图片混合排版

读者喜欢阅读图文并茂的文章,因此在应用界面中,有时不仅仅要有文字,还要有图片,这就涉及文字和图片的混排了。在图文混排过程中必然会涉及文字环绕图片的情况,很多文字处理软件(如Word、WPS、Open Office等)都有这种功能。Text Kit通过环绕路径(exclusion paths)将文字按照指定的路径环绕在图片等视图对象的 周围(见图9-8)。

图9-8 环绕路径

下面我们看看如何通过环绕路径实现文字图片混合排版。我们可以在上一节的案例基础上修改,打开Main_iPhone.storyboard故事板文件,从对象库中拖曳ImageView控件到设计视图上,如图9-9所示,通过设置Image属性设置要显示的图片为MetalType.png,当然我们之前需要将图片导入到工程中。

图像说明文字

图9-9 拖曳ImageView到设计视图

我们看看具体代码,ViewController.m文件主要代码如下:

- (void)viewDidLoad
{
    [super viewDidLoad];

    CGRect textViewRect = CGRectInset(self.view.bounds, 10.0, 20.0);  

    NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:_textView.text];

    NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];

    [textStorage addLayoutManager:layoutManager];
    _textContainer = [[NSTextContainer alloc] initWithSize:textViewRect.size];
    [layoutManager addTextContainer:_textContainer];

    [_textView removeFromSuperview];  
    _textView = [[UITextView alloc] initWithFrame:textViewRect
                                    textContainer:_textContainer];

    [self.view insertSubview:_textView belowSubview:_imageView];                         ①

    //设置凸版印刷效果
    [textStorage beginEditing];
    NSDictionary *attrsDic = @{NSTextEffectAttributeName: NSTextEffectLetterpressStyle};

    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] 
        initWithString:_textView.text attributes:attrsDic];

    [textStorage setAttributedString:attrStr];

    [textStorage endEditing];

    _textView.textContainer.exclusionPaths = @[[self translatedBezierPath]];             ②
}


- (UIBezierPath *)translatedBezierPath                                                ③
{
    CGRect imageRect = [self.textView convertRect:_imageView.frame fromView:self.view];    ④
    UIBezierPath *newPath = [UIBezierPath bezierPathWithRect:imageRect];                ⑤
    return newPath;
}

上述代码第①行是重新将TextView控件添加到View上,但是又必须考虑到不遮挡ImageView,因此需要使用ViewinsertSubview:belowSubview:方法添加,belowSubview指定ImageView,这样新添加的TextView就在ImageView之下了。第②行代码是使用TextView_textView.textContainer.exclusionPath属性指定环绕路径,该属性是NSArray类型,也就是说可以设定多个环绕路径,而且在NSArray数组中元素是一种UIBezierPath类型。

注意 UIBezierPath类可以创建基于贝塞尔曲线2路径,此类是Core Graphics框架关于图形绘制路径的一个封装,使用此类可以定义简单的形状,如椭圆、矩形,或者由多个直线和曲线段组成的形状。

2贝赛尔(Bézier)曲线是法国数学家贝塞尔在工作中发现,任何一条曲线都可以通过与它相切的控制线两端的点的位置来定义。因此,贝赛尔曲线可以用4个点描述,其中两个点描述两个端点,另外两个描述每一端的切线。贝赛尔曲线可以分为:二次方贝赛尔曲线和高阶贝赛尔曲线。

获得ImageView的贝塞尔曲线路径是通过第③行代码的translatedBezierPath方法实现的。代码中的第④行是进行坐标系转换,如图9-10所示,原来ImageView的坐标系是相对于View的坐标(62, 72),通过convertRect: fromView:方法将坐标系转换为相对于TextView的坐标(52, 52)。这是因为我们需要获得的是TextView的文字围绕ImageView路径,因此坐标系需要参照TextView

图9-10 坐标系的转换

编码完成之后我们就可以运行一下看看效果了。

9.3 动态字体

以前的iOS用户会抱怨,为什么不能设置自定义字体呢?在iOS 7系统之后苹果对于字体在显示上做了一些优化,让不同大小的字体在屏幕上都能清晰地显示。通常用户设置了自己偏好的字体了,用户可以在图9-11所示的步骤(设置→通用→辅助功能)设置粗体文字的过程。用户还可以在图9-12所示的步骤(设置→通用→文字大小)是设置文字大小的过程。

图像说明文字

图9-11 设置粗体文本

图像说明文字

图9-12 设置文字大小

但是并不是在设置中进行设置就万事大吉了,我们还要在应用代码中进行编程,以应对这些变化。我们需要在应用中给文本控件设置为用户设置的字体,而不是在代码中硬编码字体及大小。iOS 7中可以通过UIFont中新增的preferredFontForTextStyle:方法来获取用户设置的字体。

iOS 7中提供了6种字体样式供选择。

  • UIFontTextStyleHeadline。标题字体,例如:报纸的标题。
  • UIFontTextStyleSubheadline。子标题字体。
  • UIFontTextStyleBody。正文字体。
  • UIFontTextStyleFootnote。脚注字体。
  • UIFontTextStyleCaption1。标题字体,一般用于照片或者字幕。
  • UIFontTextStyleCaption2。另一个可选Caption字体。

这6种字体具体样式可见图9-13所示。

图像说明文字

图9-13 iOS系统提供的6种字体样式

处理系统提供了6种样式的字体,我们还可以自己定义字体。

当用户在设置中改变了字体,系统会给应用程序发送UIContentSizeCategoryDidChangeNotification通知,我们需要监听这个通知,并通过下面的代码重新设置文本控件字体即可。

self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];

为了能够更好地理解动态字体,下面我们通过一个实例介绍一下。我们对9.1.3节的案例修改一下,我们看看具体代码,ViewController.m文件主要代码如下:

- (void)viewDidLoad
{
    [super viewDidLoad];

    CGRect textViewRect = CGRectInset(self.view.bounds, 10.0, 20.0);    
    NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:_textView.text];

    NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];

    [textStorage addLayoutManager:layoutManager];
    _textContainer = [[NSTextContainer alloc] initWithSize:textViewRect.size];
    [layoutManager addTextContainer:_textContainer];

    [_textView removeFromSuperview];  
    _textView = [[UITextView alloc] initWithFrame:textViewRect
                                    textContainer:_textContainer];

    [self.view addSubview:_textView];

    //设置凸版印刷效果
    [textStorage beginEditing];
    NSDictionary *attrsDic = @{NSTextEffectAttributeName: NSTextEffectLetterpressStyle};

    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:
        _textView.text attributes:attrsDic]; 

    [textStorage setAttributedString:attrStr];

    [self markWord:@"我" inTextStorage:textStorage];
    [self markWord:@"I" inTextStorage:textStorage];

    [textStorage endEditing];

    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(preferredContentSizeChanged:)
            name:UIContentSizeCategoryDidChangeNotification
          object:nil];                                                        ① 

}

- (void)preferredContentSizeChanged:(NSNotification *)notification{                ②
    self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];    ③
}

在上述代码第①行是注册监听UIContentSizeCategoryDidChangeNotification通知,当系统发出这个通知后,会回调preferredContentSizeChanged:方法。代码第②行所定义的就是preferredContentSizeChanged:回调方法。在这个方法中我们通过第③行代码实现重新设置TextView的字体样式。

编码完成之后我们就可以运行一下看看效果了,如图9-14所示是运行之后通过系统设置改变文字大小前后的对比。

图像说明文字

图9-14 改变文字大小前后

在这个案例基础上大家可以改变不同的字体风格看看运行的效果。

9.4 小结

在本章中,我们首先介绍了iOS 7的Text Kit技术,通过Text Kit技术我们实现了文本图片混合排版,动态字体设置等。

目录

  • 前言
  • 第一部分 基础篇
  • 第1章 开篇综述
  • 第2章 第一个iOS应用程序
  • 第3章 UIView与控件
  • 第4章 表视图
  • 第5章 视图控制器与导航模式
  • 第6章 iOS常用设计模式
  • 第7章 iPhone与iPad应用开发的差异
  • 第8章 iOS分层架构设计
  • 第9章 iOS 7中文字排版和渲染引擎——Text Kit
  • 第10章 应用程序设置
  • 第11章 国际化
  • 第12章 数据持久化
  • 第13章 访问通讯录
  • 第二部分 网络篇
  • 第14章 访问Web Service
  • 第15章 定位服务与地图应用
  • 第三部分 进阶篇
  • 第16章 升级?
  • 第17章 iOS中的商业模式
  • 第18章 找出程序中的bug——调试
  • 第19章 测试驱动下的iOS应用开发
  • 第20章 让你的程序“飞”起来—— 性能优化
  • 第21章 管理好你的程序代码——代码版本控制
  • 第22章 把你的应用放到App Store上
  • 第四部分 实战篇
  • 第23章 重构MyNotes应用——iOS网络通信中的设计模式与架构设计
  • 第24章 iOS敏捷开发项目实战——2016里约热内卢奥运会应用开发及App Store发布