iOS文本布局探讨之——文本布局框架TextKit浅析

转载:https://yq.aliyun.com/articles/60173

https://objccn.io/issue-5-1/

摘要:在iOS开发中,使用TextKit框架进行富文本布局。TextKit有一组高级的类和协议组成,其中使用比较上层的API来完成复杂的富文本布局,功能十分强大。

iOS文本布局探讨之一——文本布局框架TextKit浅析

一、引言

在iOS开发中,处理文本的视图控件主要有4中,UILabel,UITextField,UITextView和UIWebView。其中UILabel与UITextField相对简单,UITextView是功能完备的文本布局展示类,通过它可以进行复杂的富文本布局,UIWebView主要用来加载网页或者pdf文件,其可以进行HTML,CSS和JS等文件的解析。

TextKit是一个偏上层的开发框架,在iOS7以上可用,使用它开发者可以方便灵活处理复杂的文本布局,满足开发中对文本布局的各种复杂需求。TextKit实际上是基于CoreText的一个上层框架,其是面向对象的,如果TextKit中提供的API无法满足需求,可以使用CoreText中的API进行更底层的开发。

官方文档中的一张图片很确切,经常会被用来描述TextKit框架在iOS系统文本渲染中所处的位置。

二、TextKit框架的结构

界面在进行文本的渲染时,有下面几个必要条件:

1.要渲染展示的内容。

2.将内容渲染在某个视图上。

3.内容渲染在视图上的尺寸位置和形状。

在TextKit框架中,提供了几个类分别对应处理上述的必要条件:

1.NSTextStorage对应要渲染展示的内容。

2.UITextView对应要渲染的视图。

3.NSTextContainer对应渲染的尺寸位置和形状信息。

除了上述3个类之外,TextKit框架中的NSLayoutManager类作为协调者来进行布局操作。

上述关系如下图所示:

三、使用TextKit进行文本布局流程

个人理解,TextKit主要用于更精细的处理文本布局以及进行复杂的图文混排布局,使用TextKit进行文本的布局展示十分繁琐,首先需要将显示内容定义为一个NSTextStorage对象,之后为其添加一个布局管理器对象NSLayoutManager,在NSLayoutManager中,需要进行NSTextContainer的定义,定义多了NSTextContainer对象则会将文本进行分页。最后,将要展示的NSTextContainer绑定到具体的UITextView视图上。

示例代码如下:

//定义ContainerNSTextContainer*container=[[NSTextContainer alloc]initWithSize:CGSizeMake(150,200)];//定义布局管理类NSLayoutManager*layoutManager=[[NSLayoutManager  alloc]init];//将container添加进布局管理类管理[layoutManager addTextContainer:container];//定义一个StorageNSTextStorage*storage=[[NSTextStorage alloc]initWithString:@"The NSTextContainer class defines a region where text is laid out. An NSLayoutManager uses NSTextContainer to determine where to break lines, lay out portions of text, and so on."];//为Storage添加一个布局管理器[storage addLayoutManager:layoutManager];//将要显示的container与视图TextView绑定UITextView*textView=[[UITextView alloc]initWithFrame:self.view.frame textContainer:container];[self.view addSubview:textView];

上面代码演示的过程如下图所示:

需要注意,TextKit进行布局的核心思路是最终的视图对应一个文本块Container,并不是一段文本内容Storage,LayoutManager会将完整的内容根据其中Container的尺寸进行分页,TextView根据需要显示的部分进行Container的选择。

四、了解NSTextContainer类

NSTextContainer可以简单理解为创建一个文本区块,文本内容将在这个区块中进行渲染,其中常用属性与方法如下:

//初始化方法 设置区块的尺寸-(instancetype)initWithSize:(CGSize)size;//与其绑定的layoutManager 需要注意,不是设置这个属性 使用[NSLayoutManager addTextContainer:]方式来进行绑定@property(nullable,assign,NS_NONATOMIC_IOSONLY)NSLayoutManager*layoutManager;//替换绑定的布局管理类对象-(void)replaceLayoutManager:(NSLayoutManager*)newLayoutManager;//获取区块尺寸@property(NS_NONATOMIC_IOSONLY)CGSize size;//设置从区块中剔除某一区域@property(copy,NS_NONATOMIC_IOSONLY)NSArray*exclusionPaths;//设置截断模式 需要注意 这个属性的设置只是会影响此区块的最后一行的截断模式@property(NS_NONATOMIC_IOSONLY)NSLineBreakMode lineBreakMode;//设置每行文本左右空出的间距@property(NS_NONATOMIC_IOSONLY)CGFloat lineFragmentPadding;//设置TextView上可输入的文本最大行数@property(NS_NONATOMIC_IOSONLY)NSUInteger maximumNumberOfLines;//这个方法用于提供给子类进行重写 这里返回的Rect是可以布局文本的区域-(CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect*)remainingRect;//这个BOOL值的属性决定Container的宽度是否自适应TextView的宽度@property(NS_NONATOMIC_IOSONLY)BOOL widthTracksTextView;//这个BOOL值的属性决定Container的高度是否自适应TextView的高度@property(NS_NONATOMIC_IOSONLY)BOOL heightTracksTextView;

上面所列举的方法中,exclusionPaths属性十分强大,通过设置它,可以将布局区域内剔出一块区域不进行布局,示例代码如下:

[superviewDidLoad];NSTextContainer*container=[[NSTextContainer alloc]initWithSize:CGSizeMake(300,500)];UIBezierPath*path=[UIBezierPath bezierPathWithArcCenter:self.view.center radius:70startAngle:0endAngle:M_PI*2clockwise:YES];container.exclusionPaths=@[path];container.lineBreakMode=NSLineBreakByCharWrapping;NSLayoutManager*layoutManager=[[NSLayoutManager  alloc]init];[layoutManager addTextContainer:container];NSTextStorage*storage=[[NSTextStorage alloc]initWithString:@"The NSTextContainer class defines a region where text is laid out. An NSLayoutManager uses NSTextContainer to determine where to break lines, lay out portions of text, and so on. An NSTextContainer object normally defines rectangular regions, but you can define exclusion paths inside the text container to create regions where text does not flow. You can also subclass to create text containers with nonrectangular regions, such as circular regions, regions with holes in them, or regions that flow alongside graphics.The NSTextContainer class defines a region where text is laid out. An NSLayoutManager uses NSTextContainer to determine where to break lines, lay out portions of text, and so on. An NSTextContainer object normally defines rectangular regions, but you can define exclusion paths inside the text container to create regions where text does not flow. You can also subclass to create text containers with nonrectangular regions, such as circular regions, regions with holes in them, or regions that flow alongside graphics.An NSLayoutManager uses NSTextContainer to determine where to break lines, lay out portions of text, and so on. An NSTextContainer object normally defines rectangular regions, but you can define exclusion paths inside the text container to create regions where text does not flow. You can also subclass to create text containers with nonrectangular regions, such as circular regions, regions with holes in them, or regions that flow alongside graphics."];[storage addLayoutManager:layoutManager];UITextView*textView=[[UITextView alloc]initWithFrame:self.view.frame textContainer:container];[self.view addSubview:textView];

效果如下图:

四、关于NSLayoutManager

顾名思义,NSLayoutManager专门负责对文本的布局渲染,简单理解,其从NSTextStorage从拿去展示的内容,将去处理后布局到NSTextContainer中。

NSLayoutManager与NSTextContainer的关系为一对多,放入NSLayoutManager中的NSTextContainer会以有序数组的形式进行管理,在内容布局时,超出第一个NSTextContainer的内容会被布局到后一个NSTextContainer中。

NSLayoutManager中有关NSTextContainer操作的方法如下:

//container数组@property(readonly,NS_NONATOMIC_IOSONLY)NSArray*textContainers;//添加一个container-(void)addTextContainer:(NSTextContainer*)container;//在指定位置插入一个container-(void)insertTextContainer:(NSTextContainer*)container atIndex:(NSUInteger)index;//删除一个指定的container-(void)removeTextContainerAtIndex:(NSUInteger)index;//注意 这个方法不需要显式的调用 当布局Container发生变化时 系统会自动调用-(void)textContainerChangedGeometry:(NSTextContainer*)container;

与布局管理相关的属性与方法如下:

//是否显示隐形的符号/*

默认为NO,如果设置为YES,则会将空格等隐形字符显示出来

*/@property(NS_NONATOMIC_IOSONLY)BOOL showsInvisibleCharacters;//是否显示某些布局控制字符@property(NS_NONATOMIC_IOSONLY)BOOL showsControlCharacters;//这个属性可以用于设置断字/*

这个属性的取值为0到1之间 默认为0 即单词换行时从来不会中断 越接近1 则使用连字符进行单词换行中断的概率越大

*/@property(NS_NONATOMIC_IOSONLY)CGFloat hyphenationFactor;//是否使用字体定义的行距/*

默认使用字体所定义的行距信息 通过设置这个属性为NO可以关闭此功能

*/@property(NS_NONATOMIC_IOSONLY)BOOL usesFontLeading;//这个属性设置是否允许对相邻位置的内容进行布局 默认为YES,设置为NO后将可以提供大文本布局的效率@property(NS_NONATOMIC_IOSONLY)BOOL allowsNonContiguousLayout;//下面这几个方法用于移除某一范围内的布局-(void)invalidateGlyphsForCharacterRange:(NSRange)charRange changeInLength:(NSInteger)delta actualCharacterRange:(nullable NSRangePointer)actualCharRange;-(void)invalidateLayoutForCharacterRange:(NSRange)charRange actualCharacterRange:(nullable NSRangePointer)actualCharRangeNS_AVAILABLE(10_5,7_0);-(void)invalidateDisplayForCharacterRange:(NSRange)charRange;-(void)invalidateDisplayForGlyphRange:(NSRange)glyphRange;

五、文本内容类NSTextStorage

NSTextStorage实际上是继承自NSMutableAttributedString。NSAttributedString是一种自带属性的字符串类,关于NSAttributedString的基本用法,如下博客中有介绍:

http://my.oschina.net/u/2340880/blog/397500

TextKit框架中在对文本进行布局时,主要关注于3个方面:

1.字符的属性,例如颜色,字体等。

2.行与段落的属性,如缩进,行间距等。

3.文档属性,包括四周边距、文档尺寸等。

这些都由NSAttributedString来进行定义。

如上所介绍的是TextKit框架的主要工作原理,文字渲染,图文混排的更多内容,后面博客会继续探讨。有疏漏之处,共同讨论进步。




iOS文本布局探讨之三——使用TextKit框架进行富文本布局

一、引言

关于图文混排,其实以前的博客已经讨论很多,在实际开发中,经常使用第三方的框架来完成排版的需求,其中RCLabel和RTLabel是两个比较好用的第三方库,他们的实现都是基于UIView的,通过更底层的CoreText相关API来进行图文处理。相关介绍博客地址如下:

iOS中支持HTML标签渲染的MDHTMLLaebl:http://my.oschina.net/u/2340880/blog/703254

扩展于RCLabel的支持异步加载网络图片的富文本引擎的设计:http://my.oschina.net/u/2340880/blog/499311

iOS开发封装一个可以响应超链接的label——基于RCLabel的交互扩展:http://my.oschina.net/u/2340880/blog/550194

二、原生UILabel真的只能渲染文字么?

CoreText是一个比较底层且十分强大的文本渲染框架,但是其使用起来并不是十分方便。在较低版本的iOS系统中,要进行富文本排版十分困难。在iOS6中,系统为UILabel,UITextView等这类文本渲染控件引入了NSAttributedString属性,有了NSAttributedString这个类,创建灵活多彩的文本控件变得十分轻松,开发者只需要配置NSAttributedString属性字符串即可。但是要进行图文混排,依然比较困难。iOS7之后引入TextKit框架,就完美的解决了图文混排这样的问题。

首先,iOS7中新添加了一类NSTextAttachment,从类名理解它是一个文本附件,其实也正是如此,NSTextAttachment类可以向文本中添加一些附件,这有些向邮件系统,寄信者可以向邮件中添加附件一同发送出去。NSTextAttachment类并不直接参与富文本的渲染与布局,渲染和布局依然由NSAttributedString类来完成,NSAttributedString类中提供了方法将NSTextAttachment所描述的内容转换为NSAttributedString示例。以一个简单的图文混排为例:

-(void)viewDidLoad{[superviewDidLoad];//进行NSTextAttachment的创建NSTextAttachment*attach=[[NSTextAttachment  alloc]init];//设置显示的图片attach.image=[UIImage imageNamed:@"image"];//设置尺寸attach.bounds=CGRectMake(0,0,120,60);NSTextAttachment*attach2=[[NSTextAttachment  alloc]init];attach2.image=[UIImage imageNamed:@"image2"];attach2.bounds=CGRectMake(0,0,100,90);//创建文本NSAttributedString对象NSMutableAttributedString*attri=[[NSMutableAttributedString alloc]initWithString:@"Describes a dictionary that fully specifies a font.... UIFontDescriptorInherits From NSObject UIFontDescriptor NSObject UIFontDescriptor Conforms To CVarArgT... 这里是中文"];//将NSTextAttachment映射为NSAttributedString对象NSMutableAttributedString*att=[[NSMutableAttributedString alloc]initWithAttributedString:[NSAttributedString attributedStringWithAttachment:attach]];//将图片插入NSAttributedString中[attri insertAttributedString:att atIndex:15];[attri insertAttributedString:[NSAttributedString attributedStringWithAttachment:attach2]atIndex:130];UILabel*label=[[UILabel alloc]initWithFrame:CGRectMake(20,20,280,540)];label.backgroundColor=[UIColor grayColor];label.numberOfLines=0;label.attributedText=attri;[self.view addSubview:label];}

运行工程后,效果如下图所示,其实只使用UILabel也可以实现复杂的富文本和图文混排:

三、为富文本附件添加用户交互能力

TextKit框架强大到只使用UILabel就可以完成复杂的富文本布局,但是UILabel有一个致命的缺陷,其无法进行用户交互。试想,如果可以向一段文本中添加任意数据类型的文件,当用户点击这个文件时,可以获取到文件数据并进行业务逻辑处理,这将十分酷。这样富文本布局其实就不只局限于图文混排了,我们可以插入音频,插入视频,甚至插入任意自定义格式的数据。结合使用NSTextAttachment与UITextView,这些都能实现。先看NSTextAttachment类中的一些常用属性与方法:

//这个初始化方法用于创建携带任意数据的文本附件-(instancetype)initWithData:(nullable NSData*)contentData ofType:(nullable NSString*)uti NS_DESIGNATED_INITIALIZERNS_AVAILABLE(10_11,7_0);//携带的数据内容@property(nullable,copy,NS_NONATOMIC_IOSONLY)NSData*contentsNS_AVAILABLE(10_11,7_0);//数据类型@property(nullable,copy,NS_NONATOMIC_IOSONLY)NSString*fileTypeNS_AVAILABLE(10_11,7_0);//设置渲染的图片 需要注意 如果设置的这个 附件携带的数据 fileWrapper目录内容将无效@property(nullable,strong,NS_NONATOMIC_IOSONLY)UIImage*imageNS_AVAILABLE(10_11,7_0);//设置图片渲染的尺寸@property(NS_NONATOMIC_IOSONLY)CGRect boundsNS_AVAILABLE(10_11,7_0);//设置附件携带的文件目录 需要注意 如果设置了这个属性 image和data将无效@property(nullable,strong,NS_NONATOMIC_IOSONLY)NSFileWrapper*fileWrapper;

结合UITextView可以为NSAttributedString属性字符串添加超链接,在代码回调中监听此超链接的回调可以获取NSTextAttachment携带的附件内容,如此就可以自由的进行业务处理了,示例代码如下:

-(void)viewDidLoad{[superviewDidLoad];//保留一个数组存放附件_attArray=[NSMutableArray array];//创建附件数据NSData*stringData=[NSData dataWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"image3"ofType:@"gif"]];NSTextAttachment*attach=[[NSTextAttachment  alloc]initWithData:stringData ofType:@"gif"];[_attArray addObject:attach];attach.bounds=CGRectMake(0,0,30,40);NSMutableAttributedString*attri=[[NSMutableAttributedString alloc]initWithString:@"Describes a dictionary that fully specifies a font.... UIFontDescriptorInherits From NSObject UIFontDescriptor NSObject UIFontDescriptor Conforms To CVarArgT... 这里是中文"];NSMutableAttributedString*att=[[NSMutableAttributedString alloc]initWithAttributedString:[NSAttributedString attributedStringWithAttachment:attach]];//为NSTextAttachment转换为的NSAttributedString添加超链接[att addAttributes:@{NSLinkAttributeName:@"url..."}range:NSMakeRange(0,att.string.length)];[attri insertAttributedString:att atIndex:15];UITextView*textView=[[UITextView alloc]initWithFrame:CGRectMake(20,20,280,540)];textView.backgroundColor=[UIColor grayColor];textView.dataDetectorTypes=UIDataDetectorTypeLink;textView.delegate=self;textView.attributedText=attri;textView.editable=NO;[self.view addSubview:textView];}

实现如下的TextView代理方法:

-(BOOL)textView:(UITextView*)textView shouldInteractWithURL:(NSURL*)URL inRange:(NSRange)characterRange{//可以获取到url 进行匹配NSLog(@"%@",URL);//取出NSTextAttachment附件NSTextAttachment*attach=_attArray.firstObject;NSLog(@"%@--",attach.contents);returnYES;}

向文本中添加任意数据的NSTextAttachment会展现一个文件的图标,如下图所示:

当用户点击文件图标时,会将携带的gif文件数据进行打印。

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

推荐阅读更多精彩内容