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

Text Kit学习(入门和进阶):  http://www.cocoachina.com/industry/20131028/7250.html

iOS 7中文字排版和渲染引擎——Text Kit:http://www.ituring.com.cn/tupubarticle/2542

http://www.cocoachina.com/industry/20131113/7342.html

http://www.cocoachina.com/industry/20131126/7417.html

TextKit:http://www.jianshu.com/p/2f72a5fa99f1


在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类图

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


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

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



图9-5 铅字排版1

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

9.1.4 实例:凸版印刷效果

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

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


图9-6 拖曳TextView控件

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

#import @interfaceViewController:UIViewController@property(nonatomic,strong)NSTextContainer*textContainer;①@property(strong,nonatomic)IBOutletUITextView*textView;②-(void)markWord:(NSString*)word inTextStorage:(NSTextStorage*)textStorage;③@end

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

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

-(void)viewDidLoad{[superviewDidLoad];CGRecttextViewRect=CGRectInset(self.view.bounds,10.0,20.0);①NSTextStorage*textStorage=[[NSTextStoragealloc]initWithString:_textView.text];②NSLayoutManager*layoutManager=[[NSLayoutManageralloc]init];③[textStorage addLayoutManager:layoutManager];④_textContainer=[[NSTextContaineralloc]initWithSize:textViewRect.size];⑤[layoutManager addTextContainer:_textContainer];⑥[_textView removeFromSuperview];⑦_textView=[[UITextViewalloc]initWithFrame:textViewRect

textContainer:_textContainer];⑧[self.view addSubview:_textView];⑨//设置凸版印刷效果[textStorage beginEditing];⑩NSDictionary*attrsDic=@{NSTextEffectAttributeName:NSTextEffectLetterpressStyle};NSMutableAttributedString*attrStr=[[NSMutableAttributedStringalloc]initWithString:_textView.text attributes:attrsDic];[textStorage setAttributedString:attrStr];[selfmarkWord:@"我"inTextStorage:textStorage];[selfmarkWord:@"I"inTextStorage:textStorage];[textStorage endEditing];}

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

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

第⑦~⑨行代码是重新构建原来的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=[NSRegularExpressionregularExpressionWithPattern:word

options:0error:nil];①NSArray*matches=[regex matchesInString:_textView.text

options:0range:NSMakeRange(0,[_textView.text length])];②for(NSTextCheckingResult*matchinmatches){③NSRangematchRange=[match range];[textStorage addAttribute:NSForegroundColorAttributeNamevalue:[UIColorredColor]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{[superviewDidLoad];CGRecttextViewRect=CGRectInset(self.view.bounds,10.0,20.0);NSTextStorage*textStorage=[[NSTextStoragealloc]initWithString:_textView.text];NSLayoutManager*layoutManager=[[NSLayoutManageralloc]init];[textStorage addLayoutManager:layoutManager];_textContainer=[[NSTextContaineralloc]initWithSize:textViewRect.size];[layoutManager addTextContainer:_textContainer];[_textView removeFromSuperview];_textView=[[UITextViewalloc]initWithFrame:textViewRect

textContainer:_textContainer];[self.view insertSubview:_textView belowSubview:_imageView];①//设置凸版印刷效果[textStorage beginEditing];NSDictionary*attrsDic=@{NSTextEffectAttributeName:NSTextEffectLetterpressStyle};NSMutableAttributedString*attrStr=[[NSMutableAttributedStringalloc]initWithString:_textView.text attributes:attrsDic];[textStorage setAttributedString:attrStr];[textStorage endEditing];_textView.textContainer.exclusionPaths=@[[selftranslatedBezierPath]];②}-(UIBezierPath*)translatedBezierPath③{CGRectimageRect=[self.textView convertRect:_imageView.frame fromView:self.view];④UIBezierPath*newPath=[UIBezierPathbezierPathWithRect:imageRect];⑤returnnewPath;}

上述代码第①行是重新将TextView控件添加到View上,但是又必须考虑到不遮挡ImageView,因此需要使用View的insertSubview: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=[UIFontpreferredFontForTextStyle:UIFontTextStyleBody];

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

-(void)viewDidLoad{[superviewDidLoad];CGRecttextViewRect=CGRectInset(self.view.bounds,10.0,20.0);NSTextStorage*textStorage=[[NSTextStoragealloc]initWithString:_textView.text];NSLayoutManager*layoutManager=[[NSLayoutManageralloc]init];[textStorage addLayoutManager:layoutManager];_textContainer=[[NSTextContaineralloc]initWithSize:textViewRect.size];[layoutManager addTextContainer:_textContainer];[_textView removeFromSuperview];_textView=[[UITextViewalloc]initWithFrame:textViewRect

textContainer:_textContainer];[self.view addSubview:_textView];//设置凸版印刷效果[textStorage beginEditing];NSDictionary*attrsDic=@{NSTextEffectAttributeName:NSTextEffectLetterpressStyle};NSMutableAttributedString*attrStr=[[NSMutableAttributedStringalloc]initWithString:_textView.text attributes:attrsDic];[textStorage setAttributedString:attrStr];[selfmarkWord:@"我"inTextStorage:textStorage];[selfmarkWord:@"I"inTextStorage:textStorage];[textStorage endEditing];[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(preferredContentSizeChanged:)name:UIContentSizeCategoryDidChangeNotificationobject:nil];①}-(void)preferredContentSizeChanged:(NSNotification*)notification{②self.textView.font=[UIFontpreferredFontForTextStyle:UIFontTextStyleBody];③}

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

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




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

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

9.4 小结

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







转自TracyYih的博客

更详细的内容可以参考官方文档 《Text Programming Guide for iOS》。

“Text Kit指的是UIKit框架中用于提供高质量排版服务的一些类和协议,它让程序能够存储,排版和显示文本信息,并支持排版所需要的所有特性,包括字距调整、连写、换行和对齐等。”

以前,如果我们想实现复杂的文本排版,例如在textView中显示不同样式的文本,或者图片和文字混排,你可能就需要借助于UIWebView或者深入研究一下Core Text。在iOS6中,UILabel、UITextField、UITextView增加了一个NSAttributedString属性,可以稍微解决一些排版问题,但是支持的力度还不够。现在Text Kit完全改变了这种现状。

Text Kit是基于Core Text构建的快速、先进的文本排版和渲染引擎,并且与UIKit很好的集合。UITextView,UITextField、UILabel都已经基于Text Kit重新构建,所以它们都支持分页文本、文本包装、富文本编辑、交互式文本着色、文本折叠和自定义截取等特性。所有这些UI控件现在都以同样的方式构建,在它们后面,一个NSTextStorage对象保存着文本的主要信息,它本身是NSMutableAttributedString的子类,支持分批编辑。这就意味着你可以改变一个范围内的字符的样式而不用整体替换文本内容。

[self.textView.textStorage beginEditing];

[self markWord:@"Alice"inTextStorage:self.textView.textStorage];

[self.textView.textStorage endEditing];

Text storage管理者一系列的NSLayoutManager对象,当它的字符或者属性改变时会通知到自己所管理的layout Manager对象以便它们作出相应的反应。在layout manager上面是一个NSTextContainer对象,用于为layout manager定义坐标系和一些几何特性。例如,如果你想UITextView中的文本环绕在一张图片四周,你可以给text container设定一个排除路径(exclusion path)。

UIBezierPath *exclusion = ButterflyBezierPath;

self.textView.textContainer.exclusionPaths = @[exclusion];

Text container能够处理击中测试(hit tests),所以可以定位到点击的字符在文本中的位置。此外它还提供一些代理方法让开发者能够自己定义链接点击后的处理事件。

通过基于Text Kit重新构建UILabel、UITextField和UITextView,苹果给开发者更大的灵活性和能力来设计富文本视图,同时简化了这些控件的使用,因为它们是以同样的方式设计的,所有这些好处都是站在巨人(Core Text)的肩上。通常更强大的功能和灵活性也就意味着需要更多的设置和管理,但是,如果你只是想显示一段简单的文本,你还是可以像以前一样使用。

self.textLabel.text = @"Hello Text Kit";

本文翻译自《iOS 7: Text Kit

Text Kit进阶

上一篇文章Text Kit入门中我们主要了解了什么是Text Kit及它的一些架构和基本特性,这篇文章中会涉及关于Text Kit的更多具体应用。

Text Kit是建立在Core Text框架上的,我们知道CoreText.framework是一个庞大而复杂的框架,而Text Kit在继承了Core Text强大功能的同时给开发者提供了比较友好的面向对象的API。

本文主要介绍Text Kit下面四个特性:

动态字体(Dynamic type)

凸版印刷体效果(Letterpress effects)

路径排除(Exclusion paths)

动态文本格式化和存储(Dynamic text formatting and storage)

动态字体(Dynamic type)

动态字体是iOS7中新增加的比较重要的特性之一,程序应该按照用户设定的字体大小和粗细来显示文本内容。

分别在设置\通用\辅助功能和设置\通用\文字大小中可以设置文本在应用程序中显示的粗细和大小。

iOS7对系统字体在显示上做了一些优化,让不同大小的字体在屏幕上都能清晰的显示。通常用户设置了自己偏好的字体,他们希望在所有程序中都看到文本显示是根据他们的设定进行调整。为了实现这个,开发者需要在自己的应用中给文本控件设置当前用户设置字体,而不是指定死字体及大小。可以通过UIFont中新增的preferredFontForTextStyle:方法来获取用户偏好的字体。

iOS7中给出了6中字体样式供选择:

UIFontTextStyleHeadline

UIFontTextStyleBody

UIFontTextStyleSubheadline

UIFontTextStyleFootnote

UIFontTextStyleCaption1

UIFontTextStyleCaption2

为了让我们的程序支持动态字体,需要按一下方式给文本控件(通常是指UILabel,UITextField,UITextView)设定字体:

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

这样设置之后,文本控件就会以用户设定的字体大小及粗细显示,但是如果程序在运行时,用户切换到设置里修改了字体,这是在切回程序,字体并不会自动跟着变。这时就需要我们自己来更新一下控件的字体了。

在系统字体修改时,系统会给运行中的程序发送UIContentSizeCategoryDidChangeNotification通知,我们只需要监听这个通知,并重新设置一下字体即可。

[[NSNotificationCenter defaultCenter] addObserver:self

selector:@selector(preferredContentSizeChanged:)

name:UIContentSizeCategoryDidChangeNotification

object:nil];

- (void)preferredContentSizeChanged:(NSNotification *)notification{

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

}

当然,有的时候要适应动态修改的字体并不是这么设置一下就完事了,控件的大小可能也需要进行相应的调整,这时我们程序中的控件大小也不应该写死,而是需要根据字体大小来计算.

凸版印刷体效果(Letterpress effects)

凸版印刷替效果是给文字加上奇妙阴影和高光,让文字看起有凹凸感,像是被压在屏幕上。当然这种看起来很高端大气上档次的效果实现起来确实相当的简单,只需要给AttributedString加一个NSTextEffectAttributeName属性,并指定该属性值为NSTextEffectLetterpressStyle就可以了。

tionary *attributes = @{

NSForegroundColorAttributeName: [UIColor redColor],

NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline],

NSTextEffectAttributeName: NSTextEffectLetterpressStyle

};

self.titleLabel.attributedText = [[NSAttributedString alloc] initWithString:@"Title"attributes:attributes];

在iOS7系统自带的备忘录应用中,苹果就使用了这种凸版印刷体效果。

路径排除(Exclusion paths)

在排版中,图文混排是非常常见的需求,但有时候我们的图片并一定都是正常的矩形,这个时候我们如果需要将文本环绕在图片周围,就可以用路径排除(exclusion paths)了。

Explosion pats基本原理是将需要被文本留出来的形状的路径告诉文本控件的NSTextContainer对象,NSTextContainer在文字排版时就会避开该路径。

UIBezierPath *floatingPath = [self pathOfImage];

self.textView.textContainer.exclusionPaths = @[floatingPath];

所以实现Exclusion paths的主要工作就是获取这个path。

动态文本格式化和存储(Dynamic text formatting and storage)

好了,到现在我们知道了Text Kit可以动态的根据用户设置的字体大小进行调整,但是如果具体某个文本显示控件中的文本样式能够动态调整是不是会更酷一些呢?

实现这些才是真正体现Text Kit强大之处的时候,在此之前你需要理解Text Kit中的文本存储系统是怎么工作的,下图显示了Text Kit中文本的保存、渲染和现实之间的关系。

当你使用UITextView、UILabel、UITextField控件的时候,系统会自动创建上面这些类,你可以选择直接使用这么默认的实现或者为你的控件自定义这几个中的任何一个。

1.NSTextStorage本身继承与NSMutableAttributedString,它是以attributed string的形式保存需要渲染的文本,并在文本内容改变的时候通知到对应的layout manager对象。通常你需要创建NSTextStorage的子类来在文本改变时进行文本显示样式的更新。

2.NSLayoutManager作为文本控件中的排版引擎接收保存的文本并在屏幕上渲染出来。

3.NSTextContainer描述了文本在屏幕上显示时的几何区域,每个text container与一个具体的UITextView相关联。如果你需要定义一个很复杂形状的区域来显示文本,你可能需要创建NSTextContainer子类。

要实现我们上面描述的动态文本格式化功能,我们需要创建NSTextStorage子类以便在用户输入文本的时候动态的增加文本属性。自定义了text storage后,我们需要替换调UITextView默认的text storage。

创建NSTextStorage的子类

我们创建NSTextStorage子类,命名为MarkupTextStorage,在实现文件中添加一个成员变量:

#import "MarkupTextStorage.h"

@implementation MarkupTextStorage

{

NSMutableAttributedString *_backingStore;

}

- (id)init

{

self = [superinit];

if(self) {

_backingStore = [[NSMutableAttributedString alloc] init];

}

returnself;

}

@end

NSTextStorage的子类需要重载一些方法提供NSMutableAttributedString类型的backing store信息,所以我们继续添加下面代码:

- (NSString *)string

{

return[_backingStore string];

}

- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range

{

return[_backingStore attributesAtIndex:location effectiveRange:range];

}

- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str

{

[self beginEditing];

[_backingStore replaceCharactersInRange:range withString:str];

[self edited:NSTextStorageEditedCharacters | NSTextStorageEditedAttributes

range:range changeInLength:str.length - range.length];

[self endEditing];

}

- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range

{

[self beginEditing];

[_backingStore setAttributes:attrs range:range];

[self edited:NSTextStorageEditedAttributes

range:range changeInLength:0];

[self endEditing];

}

后面两个方法都是代理到backing store,然后需要被beginEditing edited endEditing包围,而且必须在文本编辑时按顺序调用来通知text storage对应的layout manager。

你可能发现子类化NSTextStorage需要写不少的代码,因为NSTextStorage是一个类集群中的一个开发接口,不能只是继承它然后重载很少的方法来拓展它的功能,而是需要自己实现很多细节。

类集群(Class cluster)是苹果Cocoa(Touch)框架中常用的设计模式之一。

类集群是Objective-C中对抽象工厂模式的简单实现,为创建一些列相关或独立对象提供了统一的接口而不用指定具体的类。常用的像NSArray和NSNumber事实上也是一系列类集群的开放接口。

苹果使用类集群是为了将一些类具体类隐藏在开放的抽象父类之下,外面通过抽象父类的方法来创建私有子类的实例,并且外界也完全不知道工厂分配到了哪个私有类,因为它们始终只和开放接口交互。

使用类集群确实简化了接口,让类更容易被使用,但是要知道鱼和熊掌不可兼得,你又想简单又想可拓展性强,哪有那么好的事啊?所以创建一个类集群中的抽象父类就没有那么简单了。

好了,上面解释了这么多其实主要就说明了为什么子类化NSTextStorage需要写这么多代码,下面要在UITextView使用我们自定义的text storage了。

设置UITextView

- (void)createMarkupTextView

{

NSDictionary *attributes = @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]};

NSString *content = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"content"ofType:@"txt"]

encoding:NSUTF8StringEncoding

error:nil];

NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:content

attributes:attributes];

_textStorage = [[MarkupTextStorage alloc] init];

[_textStorage setAttributedString:attributedString];

CGRect textViewRect = CGRectMake(20, 60, 280, self.view.bounds.size.height - 100);

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

NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(textViewRect.size.width, CGFLOAT_MAX)];

[layoutManager addTextContainer:textContainer];

[_textStorage addLayoutManager:layoutManager];

_textView = [[UITextView alloc] initWithFrame:textViewRect

textContainer:textContainer];

_textView.delegate = self;

[self.view addSubview:_textView];

}

很长的代码,下面我们来看看都做了些啥:

1.创建了一个自定义的text storage对象,并通过attributed string保存了需要显示的内容;

2.创建了一个layout manager对象;

3.创建了一个text container对象并将它与layout manager关联,然后该text container再和text storage对象关联;

4.通过text container创建了一个text view并显示。

你可以将代码和前面那对象间的关系图对应着理解一下。

动态格式化

继续在MarkupTextStorage.m文件中添加如下方法:

- (void)processEditing

{

[self performReplacementsForRange:[self editedRange]];

[superprocessEditing];

}

processEditing在layout manager中文本修改时发送通知,它通常也是处理一些文本修改逻辑的好地方。

继续添加:

- (void)performReplacementsForRange:(NSRange)changedRange

{

NSRange extendedRange = NSUnionRange(changedRange, [[_backingStore string]

lineRangeForRange:NSMakeRange(changedRange.location, 0)]);

extendedRange = NSUnionRange(changedRange, [[_backingStore string]

lineRangeForRange:NSMakeRange(NSMaxRange(changedRange), 0)]);

[self applyStylesToRange:extendedRange];

}

这个方法用于扩大文本匹配的范围,因为changedRange只是标识出一个字符,lineRangeForRange会将范围扩大到当前的一整行。

下面就剩下匹配特定格式的文本来显示对应的样式了:

- (NSDictionary*)createAttributesForFontStyle:(NSString*)style

withTrait:(uint32_t)trait {

UIFontDescriptor *fontDescriptor = [UIFontDescriptor

preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];

UIFontDescriptor *descriptorWithTrait = [fontDescriptor

fontDescriptorWithSymbolicTraits:trait];

UIFont* font =  [UIFont fontWithDescriptor:descriptorWithTrait size: 0.0];

return@{ NSFontAttributeName : font };

}

- (void)createMarkupStyledPatterns

{

UIFontDescriptor *scriptFontDescriptor =

[UIFontDescriptor fontDescriptorWithFontAttributes:

@{UIFontDescriptorFamilyAttribute: @"Bradley Hand"}];

// 1. base our script font on the preferred body font size

UIFontDescriptor* bodyFontDescriptor = [UIFontDescriptor

preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];

NSNumber* bodyFontSize = bodyFontDescriptor.

fontAttributes[UIFontDescriptorSizeAttribute];

UIFont* scriptFont = [UIFont

fontWithDescriptor:scriptFontDescriptor size:[bodyFontSize floatValue]];

// 2. create the attributes

NSDictionary* boldAttributes = [self

createAttributesForFontStyle:UIFontTextStyleBody

withTrait:UIFontDescriptorTraitBold];

NSDictionary* italicAttributes = [self

createAttributesForFontStyle:UIFontTextStyleBody

withTrait:UIFontDescriptorTraitItalic];

NSDictionary* strikeThroughAttributes = @{ NSStrikethroughStyleAttributeName : @1,

NSForegroundColorAttributeName: [UIColor redColor]};

NSDictionary* scriptAttributes = @{ NSFontAttributeName : scriptFont,

NSForegroundColorAttributeName: [UIColor blueColor]

};

NSDictionary* redTextAttributes =

@{ NSForegroundColorAttributeName : [UIColor redColor]};

_replacements = @{

@"(\\*\\*\\w+(\\s\\w+)*\\*\\*)": boldAttributes,

@"(_\\w+(\\s\\w+)*_)": italicAttributes,

@"(~~\\w+(\\s\\w+)*~~)": strikeThroughAttributes,

@"(`\\w+(\\s\\w+)*`)": scriptAttributes,

@"\\s([A-Z]{2,})\\s": redTextAttributes

};

}

- (void)applyStylesToRange:(NSRange)searchRange

{

NSDictionary* normalAttrs = @{NSFontAttributeName:

[UIFont preferredFontForTextStyle:UIFontTextStyleBody]};

// iterate over each replacement

for(NSString* keyin_replacements) {

NSRegularExpression *regex = [NSRegularExpression

regularExpressionWithPattern:key

options:0

error:nil];

NSDictionary* attributes = _replacements[key];

[regex enumerateMatchesInString:[_backingStore string]

options:0

range:searchRange

usingBlock:^(NSTextCheckingResult *match,

NSMatchingFlags flags,

BOOL *stop){

// apply the style

NSRange matchRange = [match rangeAtIndex:1];

[self addAttributes:attributes range:matchRange];

// reset the style to the original

if(NSMaxRange(matchRange)+1 < self.length) {

[self addAttributes:normalAttrs

range:NSMakeRange(NSMaxRange(matchRange)+1, 1)];

}

}];

}

}

在createMarkupStyledPatterns初始化方法中调用createMarkupStyledPatterns,通过正则表达式来给特定格式的字符串设定特定显示样式,形成一个对应的字典。然后在applyStylesToRange:中利用已定义好的样式字典来给匹配的文本端增加样式。

到这里本篇文章的内容就结束了,其实前面三点都很简单,稍微过一下就能用。最后一个动态文本格式化内容稍微多一点,可以结合我的代码TextKitDemo来看。

参考链接:

http://www.raywenderlich.com/50151/text-kit-tutorial

https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/Introduction/Introduction.html

http://adcdownload.apple.com/wwdc_2013/wwdc_2013_sample_code/ios_intrototextkit.zi


Text Kit是iOS 7中引入的一个新功能,非常值得开发者使用,下面先看看本文的目录结构:

什么是Text Kit

Text Kit架构

Text Kit特点

Text Kit功能概述

Text Kit中重要的一些对象

Text Kit示例

小结

推荐Text Kit学习资源

什么是Text Kit

在iOS7中,苹果引入了Text Kit--Text Kit是一个快速而又现代化的文字排版和渲染引擎。Text Kit在UIKit framework中的定义了一些类和相关协议,它最主要的作用就是为程序提供文字排版和渲染的功能。在程序中,通过Text Kit可以对文字进行存储(store)、布局(lay out),以及用最精细的排版方式(例如文字间距、换行和对齐等)来显示文本内容。 苹果引入Text Kit的目的并非要取代已有的Core Text,Core Text的主要作用也是用于文字的排版和渲染中,它是一种先进而又处于底层技术,如果我们需要将文本内容直接渲染到图形上下文(Graphics context)时,从性能和易用性来考虑,最佳方案就是使用Core Text。而如果我们直接利用苹果提供的一些控件(例如UITextView、UILabel和UITextField等)对文字进行排版,无疑就是借助于UIkit framework中Text Kit提供的API。

Text Kit架构

下面,我们通过图1(此图来自WWDC2013 Session 210)来了解一下Text Kit的架构。图1是基于iOS 7绘制的,从图中,我们可以看到Text Kit是基于Core Text构建的,它通过Core Text与Core Graphics进行交互。而UI控件(UILabel、UITextField和UITextView)则构建于Text Kit之上,可见这些文本控件可以利用Text Kit提供的API来对文字进行排版和渲染处理。 从图中我们也可以看到SDK提供的UIWebView是基于WebKit的,它不能使用Text Kit提供的功能。

图1 Text Kit在iOS 7 SDK中的位置

我们再来看看图1中的相关组件在iOS6里面是如何对应的,如图2所示,可以看出在iOS 6中是没有Text Kit,并且UILabel、UIText和UITextView是基于String Drawing和WebKit构建的。其中String Drawing是与Core Graphics直接通讯。

图2 在iOS 6中并没有Text Kit

Text Kit特点

从上面的介绍中,我们可以了解到Text Kit在UIKit中的作用非常重要。Text Kit在实际开发中具有如下特点:

1.在UI控件中Text Kit完全掌控着文字的排版和渲染

2.UITextView、UITextField和UILabel是构建于Text Kit之上的

3.能够与动画、UICollectionView和UITableView做到无缝集成

4.Text Kit具有这样一些能力:Subclassing、Delegation和Notifcation。

Text Kit功能概述

下面我们看看通过Text Kit,都能实现那些功能(这里列出了是一些常用和重要功能):

1.对文字进行分页或多列排版

2.支持文字的换行、折叠和着色等处理

3.可以调整字与字之间的距离、行间距、文字大小、指定特定的字体

4.支持富文本编辑,可以自定义文字截断

5.支持凸版印刷效果(letterpress)

6.支持数据类型的检测(例如链接、附件等)

如图3,是利用Text Kit对文字做的分页排版

图3 利用Text Kit做的分页排版效果

再看图4,是利用Text Kit做的换行处理,其中对某个路径范围做了排除。

图4 利用Text Kit做的换行处理效果

再来看看利用Text Kit做的凸版印刷效果,如图5所示

图5 利用Text Kit做的凸版印刷效果

Text Kit中重要的一些对象

下面我们来看看Text Kit中重要的几个对象。

图6 Text Kit中重要的几个对象

如图6所示,Text Kit中主要有4个重要的对象。

1.Text View是用来显示文本内容的控件,主要包括UILabel、UITextView和UITextField。

2.Text containers对应着NSTextContainer类。NSTextContainer定义了文本可以排版的区域。一般来说,都是矩形区域,当然,也可以根据需求,通过子类化NSTextContainer来创建别的一些形状,例如圆形、不规则的形状等。NSTextContainer不仅可以创建文本可以填充的区域,它还维护着一个数组——该数组定义了一个区域,排版的时候文字不会填充该区域,因此,我们可以在排版文字的时候,填充非文本元素(例如图片,如图4所示)。

3.Layout manager对应着NSLayoutManager类。该类负责对文字进行编辑排版处理——通过将存储在NSTextStorage中的数据转换为可以在视图控件中显示的文本内容,并把统一的字符编码映射到对应的字形(glyphs)上,然后将字形排版到NSTextContainer定义的区域中。

4.Text storage对应着NSTextStorage类。该类定义了Text Kit扩展文本处理系统中的基本存储机制。NSTextStorage继承自NSmutableAttributedString,主要用来存储文本的字符和相关属性。另外,当NSTextStorage中的字符或属性发生了改变,会通知NSLayoutManager,进而做到文本内容的显示更新。

通常情况下,NSTextStorage、NSLayoutManager和NSTextContainer是一一对应的。如图7所示关系:

图7 普通排版

当然,如果需要将文字显示为多列,或多页,可以按照如图8所示关系——使用多个NSTextContainer。

图8 多页或者多列排版

如果针对不同的排版方式,则可以使用多个NSLayoutManager,如图9所示

图9 不同的排版方式

如图10所示,通过形象的方式,对UITextView的组成做了分解。通常,我们在设备上只能看到最右边的文本显示界面,而内部的NSTextStorage、NSLayoutManager和NSTextContainer是看不出来的。通常由NSLayoutManager从NSTextStorage中读取出文本数据,然后根据一定的排版方式,将文本排版到NSTextContainer中,再由NSTextContainer结合UITextView将最终效果显示出来。

图10 UITextView的分解

Text Kit示例

前面对Text Kit做了一些介绍,下面我们配合一个例子(图文排版),来进一步加深对Text Kit的认识。具体实现步骤如下:

1.打开Xcode 5,新建一个Single View Application模板的程序,将工程命名为ExclusionPath。

2.打开Main.storyboard文件,然后再默认View Controller的View里面分别添加一个UITextView和UIImageView。并将这两个控件连接到ViewController.h中(名称分别为textView何imageView)。然后给textView设置一些字符串,imageView设置一个图片。

3.打开ViewController.m文件,找到viewDidLoad方法,用如下代码替换该方法:

- (void)viewDidLoad

{

[superviewDidLoad];

//创建一个平移手势对象,该对象可以调用imagePanned:方法

UIPanGestureRecognizer *panGes = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(imagePanned:)];

[self.imageView addGestureRecognizer:panGes];

self.textView.textContainer.exclusionPaths = @[[self translatedBezierPath]];

}

在上面的代码中,给imageView添加了一个平移手势。另外通过调用translatedBezierPath方法,给textView的textContainer设置exclusionPaths属性值。表示需要排除的区域(也就是图片在排版中显示的位置)。

下面来看一下translatedBezierPath方法的实现,如下代码所示

- (UIBezierPath *)translatedBezierPath

{

CGRect butterflyImageRect = [self.textView convertRect:self.imageView.frame fromView:self.view];

UIBezierPath *newButterflyPath = [UIBezierPath bezierPathWithRect:butterflyImageRect];

returnnewButterflyPath;

}

在上面的代码中,利用imageView的frame属性创建了一个UIBezierPath,然后将该值返回。 5. 还记得第3步中创建的平移手势吗。里面有一个action需要实现imagePanned:,下面来看看这个方法的实现:

- (void)imagePanned:(id)sender

{

if([sender isKindOfClass:[UIPanGestureRecognizerclass]]) {

UIPanGestureRecognizer *localSender = sender;

if(localSender.state == UIGestureRecognizerStateBegan) {

self.gestureStartingPoint = [localSender translationInView:self.textView];

self.gestureStartingCenter = self.imageView.center;

}elseif(localSender.state == UIGestureRecognizerStateChanged) {

CGPoint currentPoint = [localSender translationInView:self.textView];

CGFloat distanceX = currentPoint.x - self.gestureStartingPoint.x;

CGFloat distanceY = currentPoint.y - self.gestureStartingPoint.y;

CGPoint newCenter = self.gestureStartingCenter;

newCenter.x += distanceX;

newCenter.y += distanceY;

self.imageView.center = newCenter;

self.textView.textContainer.exclusionPaths = @[[self translatedBezierPath]];

}elseif(localSender.state == UIGestureRecognizerStateEnded) {

self.gestureStartingPoint = CGPointZero;

self.gestureStartingCenter = CGPointZero;

}

}

}

在上面的代码中首先根据平移的距离来设置imageView的位置,然后利用translatedBezierPath方法重新计算了一下排除区域。 6. 至此代码编写完毕,下面来运行程序,看看实际效果。如图11所示:

图11 运行效果

点击下图,下载代码

小结

实际上,上面的示例,只是揭秘了Text Kit功能的冰山一角。从iOS7及以后的版本中,Text Kit在UIKit framework里面占据重要的地位,Text Kit在文字处理方面,具有非常强大的功能,并且开发者可以对Text Kit进行定制和扩展。据悉,苹果利用了2年的时间来开发Text Kit,相信这对许多开发者来说都是福音。

推荐Text Kit学习资源

更多关于Text Kit的学习资料,请参考下面的内容:

wwdc视频:

Introducing Text Kit

Advanced Text Layouts and Effects with Text Kit

Using Fonts with Text Kit

苹果官方参考文档

Text Programming Guide for iOS.pdf

NSLayoutManager Class Reference for iOS.pdf

NSLayoutManagerDelegate Protocol Reference for iOS.pdf

NSTextContainer Class Reference for iOS.pdf

NSTextStorage Class Reference for iOS.pdf

NSTextStorageDelegate Protocol Reference for iOS.pdf

苹果官方示例:

IntroToTextKit

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

推荐阅读更多精彩内容