在上一篇文章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

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

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

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

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

  1. [[NSNotificationCenter defaultCenter] addObserver:self
  2. selector:@selector(preferredContentSizeChanged:)
  3. name:UIContentSizeCategoryDidChangeNotification
  4. object:nil];
  1. - (void)preferredContentSizeChanged:(NSNotification *)notification{
  2. self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
  3. }

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

Letterpress effects

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

  1. NSDictionary *attributes = @{
  2. NSForegroundColorAttributeName: [UIColor redColor],
  3. NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline],
  4. NSTextEffectAttributeName: NSTextEffectLetterpressStyle
  5. };
  6. self.titleLabel.attributedText = [[NSAttributedString alloc] initWithString:@"Title" attributes:attributes];

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

Exclusion paths

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

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

  1. UIBezierPath *floatingPath = [self pathOfImage];
  2. self.textView.textContainer.exclusionPaths = @[floatingPath];

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

Dynamic text formatting and storage

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

例如,你希望让你的textView中的文本自动支持下面功能:

  • **字符之间的文本加粗显示
  • _字符之间的文本以斜体字显示
  • ~~字符之间的文本以被横线穿透样式显示
  • 让全大写的文本以红色字体显示

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

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

  • NSTextStorage本身继承与NSMutableAttributedString,它是以attributed string的形式保存需要渲染的文本,并在文本内容改变的时候通知到对应的layout manager对象。通常你需要创建NSTextStorage的子类来在文本改变时进行文本显示样式的更新。
  • NSLayoutManager作为文本控件中的排版引擎接收保存的文本并在屏幕上渲染出来。
  • NSTextContainer描述了文本在屏幕上显示时的几何区域,每个text container与一个具体的UITextView相关联。如果你需要定义一个很复杂形状的区域来显示文本,你可能需要创建NSTextContainer子类。

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

创建NSTextStorage的子类

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

  1. #import "MarkupTextStorage.h"
  2.  
  3. @implementation MarkupTextStorage
  4. {
  5. NSMutableAttributedString *_backingStore;
  6. }
  7.  
  8. - (id)init
  9. {
  10. self = [super init];
  11. if (self) {
  12. _backingStore = [[NSMutableAttributedString alloc] init];
  13. }
  14. return self;
  15. }
  16.  
  17. @end

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

  1. - (NSString *)string
  2. {
  3. return [_backingStore string];
  4. }
  5.  
  6. - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
  7. {
  8. return [_backingStore attributesAtIndex:location effectiveRange:range];
  9. }
  10.  
  11. - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
  12. {
  13. [self beginEditing];
  14. [_backingStore replaceCharactersInRange:range withString:str];
  15. [self edited:NSTextStorageEditedCharacters | NSTextStorageEditedAttributes
  16. range:range changeInLength:str.length - range.length];
  17. [self endEditing];
  18. }
  19.  
  20. - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
  21. {
  22. [self beginEditing];
  23. [_backingStore setAttributes:attrs range:range];
  24. [self edited:NSTextStorageEditedAttributes
  25. range:range changeInLength:0];
  26. [self endEditing];
  27. }

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

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

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

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

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

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

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

设置UITextView

  1. - (void)createMarkupTextView
  2. {
  3. NSDictionary *attributes = @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]};
  4. NSString *content = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"content" ofType:@"txt"]
  5. encoding:NSUTF8StringEncoding
  6. error:nil];
  7. NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:content
  8. attributes:attributes];
  9. _textStorage = [[MarkupTextStorage alloc] init];
  10. [_textStorage setAttributedString:attributedString];
  11.  
  12. CGRect textViewRect = CGRectMake(20, 60, 280, self.view.bounds.size.height - 100);
  13.  
  14. NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
  15.  
  16. NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(textViewRect.size.width, CGFLOAT_MAX)];
  17. [layoutManager addTextContainer:textContainer];
  18. [_textStorage addLayoutManager:layoutManager];
  19.  
  20. _textView = [[UITextView alloc] initWithFrame:textViewRect
  21. textContainer:textContainer];
  22. _textView.delegate = self;
  23. [self.view addSubview:_textView];
  24. }

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

  1. 创建了一个自定义的text storage对象,并通过attributed string保存了需要显示的内容;
  2. 创建了一个layout manager对象;
  3. 创建了一个text container对象并将它与layout manager关联,然后该text container再和text storage对象关联;
  4. 通过text container创建了一个text view并显示。

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

动态格式化

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

  1. - (void)processEditing
  2. {
  3. [self performReplacementsForRange:[self editedRange]];
  4. [super processEditing];
  5. }

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

继续添加:

  1. - (void)performReplacementsForRange:(NSRange)changedRange
  2. {
  3. NSRange extendedRange = NSUnionRange(changedRange, [[_backingStore string]
  4. lineRangeForRange:NSMakeRange(changedRange.location, 0)]);
  5. extendedRange = NSUnionRange(changedRange, [[_backingStore string]
  6. lineRangeForRange:NSMakeRange(NSMaxRange(changedRange), 0)]);
  7. [self applyStylesToRange:extendedRange];
  8. }

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

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

  1. - (NSDictionary*)createAttributesForFontStyle:(NSString*)style
  2. withTrait:(uint32_t)trait {
  3. UIFontDescriptor *fontDescriptor = [UIFontDescriptor
  4. preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];
  5.  
  6. UIFontDescriptor *descriptorWithTrait = [fontDescriptor
  7. fontDescriptorWithSymbolicTraits:trait];
  8.  
  9. UIFont* font = [UIFont fontWithDescriptor:descriptorWithTrait size: 0.0];
  10. return @{ NSFontAttributeName : font };
  11. }
  12.  
  13. - (void)createMarkupStyledPatterns
  14. {
  15. UIFontDescriptor *scriptFontDescriptor =
  16. [UIFontDescriptor fontDescriptorWithFontAttributes:
  17. @{UIFontDescriptorFamilyAttribute: @"Bradley Hand"}];
  18.  
  19. // 1. base our script font on the preferred body font size
  20. UIFontDescriptor* bodyFontDescriptor = [UIFontDescriptor
  21. preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];
  22. NSNumber* bodyFontSize = bodyFontDescriptor.
  23. fontAttributes[UIFontDescriptorSizeAttribute];
  24. UIFont* scriptFont = [UIFont
  25. fontWithDescriptor:scriptFontDescriptor size:[bodyFontSize floatValue]];
  26.  
  27. // 2. create the attributes
  28. NSDictionary* boldAttributes = [self
  29. createAttributesForFontStyle:UIFontTextStyleBody
  30. withTrait:UIFontDescriptorTraitBold];
  31. NSDictionary* italicAttributes = [self
  32. createAttributesForFontStyle:UIFontTextStyleBody
  33. withTrait:UIFontDescriptorTraitItalic];
  34. NSDictionary* strikeThroughAttributes = @{ NSStrikethroughStyleAttributeName : @1,
  35. NSForegroundColorAttributeName: [UIColor redColor]};
  36. NSDictionary* scriptAttributes = @{ NSFontAttributeName : scriptFont,
  37. NSForegroundColorAttributeName: [UIColor blueColor]
  38. };
  39. NSDictionary* redTextAttributes =
  40. @{ NSForegroundColorAttributeName : [UIColor redColor]};
  41.  
  42. _replacements = @{
  43. @"(\\*\\*\\w+(\\s\\w+)*\\*\\*)" : boldAttributes,
  44. @"(_\\w+(\\s\\w+)*_)" : italicAttributes,
  45. @"(~~\\w+(\\s\\w+)*~~)" : strikeThroughAttributes,
  46. @"(`\\w+(\\s\\w+)*`)" : scriptAttributes,
  47. @"\\s([A-Z]{2,})\\s" : redTextAttributes
  48. };
  49. }
  50.  
  51. - (void)applyStylesToRange:(NSRange)searchRange
  52. {
  53. NSDictionary* normalAttrs = @{NSFontAttributeName:
  54. [UIFont preferredFontForTextStyle:UIFontTextStyleBody]};
  55.  
  56. // iterate over each replacement
  57. for (NSString* key in _replacements) {
  58. NSRegularExpression *regex = [NSRegularExpression
  59. regularExpressionWithPattern:key
  60. options:0
  61. error:nil];
  62.  
  63. NSDictionary* attributes = _replacements[key];
  64.  
  65. [regex enumerateMatchesInString:[_backingStore string]
  66. options:0
  67. range:searchRange
  68. usingBlock:^(NSTextCheckingResult *match,
  69. NSMatchingFlags flags,
  70. BOOL *stop){
  71. // apply the style
  72. NSRange matchRange = [match rangeAtIndex:1];
  73. [self addAttributes:attributes range:matchRange];
  74.  
  75. // reset the style to the original
  76. if (NSMaxRange(matchRange)+1 < self.length) {
  77. [self addAttributes:normalAttrs
  78. range:NSMakeRange(NSMaxRange(matchRange)+1, 1)];
  79. }
  80. }];
  81. }
  82. }

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


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

参考链接:

Posted by TracyYih - Oct 17 2013
如需转载,请注明: 本文来自 Esoft Mobile

Text Kit进阶的更多相关文章

  1. 关于Text Kit 一些事

    1. Text Kit 是什么? 在iOS7中,苹果引入了Text Kit--Text Kit是一个高速而又现代化的文字排版和渲染引擎.Text Kit在UIKit framework中的定义了一些类 ...

  2. iOS富文本(三)深入使用Text Kit

    在上一篇中介绍了Text Kit的三种基本组件的关系并且简单的实现了怎么使用这三种基本组件,本片将深入的去使用这三种基本组件. NSTextStorage NSTextStorage是NSMutabl ...

  3. iOS富文本(二)初识Text Kit

    概述 Text Kit 是建立在Core Text上的文本布局系统,虽然没有Core Text那么强大的文本处理功能,但是对于大多数常见的文本布局用Text Kit能够很简单的实现,而不是用Core ...

  4. Text Kit入门

    更详细的内容可以参考官方文档 <Text Programming Guide for iOS>. “Text Kit指的是UIKit框架中用于提供高质量排版服务的一些类和协议,它让程序能够 ...

  5. ios7新特性1-UI变化、UIKit动态行为支持与Text Kit新接口

    iOS 7.0新特性1 iOS 7的UI经过了重新设计.另外,iOS7中引入了新的动画系统,便于创建2D和2.5D的游戏.多任务支持提升,点对点通讯以及其他重要的特征使iOS7相对于以往的SDK来说发 ...

  6. textkit

    更详细的内容可以参考官方文档 <Text Programming Guide for iOS>. “Text Kit指的是UIKit框架中用于提供高质量排版服务的一些类和协议,它让程序能够 ...

  7. Core Text概述

    本文是我翻译的苹果官方文档<Core Text Overview> Core Text框架是高级的底层文字布局和处理字体的技术.它在Mac OS X v10.5 and iOS 3.2开始 ...

  8. CoreText学习(一)Base Objects of Core Text

    最近要做一个读入Word,PDF格式等的文件并且加以编辑的程序,本来以为使用Text Kit结合Text View来打开doc文件是完全没问题的,结果用了各种方法打开要么是数据是nil,要么打开的文字 ...

  9. Text Relatives

    [Text Relatives] With TextKit the resources at your disposal range from framework objects—such as te ...

随机推荐

  1. cojs 安科赛斯特 题解报告

    QAQ 从IOI搬了一道题目过来 官方题解貌似理论上没有我的做法优,我交到BZOJ上也跑的飞快 结果自己造了个数据把自己卡成了4s多,真是忧桑的故事 不过貌似原题是交互题,并不能离线 说说我的做法吧 ...

  2. cojs 白树黑 黑树白 题解报告

    黑树白 首先如果不是强制在线,这个题用莫队+树状数组就可以在O(n*sqrt(n)*log(n))的时间内搞定 如果没有修改操作,可以直接上主席树就可以辣 我们考虑修改操作,某一个修改操作对于某一个查 ...

  3. 51Nod 算法马拉松12 Rikka with sequences

    当时做比赛的时候听说过这类用KD_Tree维护的数据结构题 然后知道是KD_Tree,然而并不知道怎么写QAQ 比赛完了之后%了一发代码 其基本思路是这样的: 1.首先我们把询问[L,R]看成二维平面 ...

  4. 李洪强漫谈iOS开发[C语言-037]-if else 语句

    李洪强漫谈iOS开发[C语言-037]-if else 语句

  5. lintcode :Remove Duplicates from Sorted List 删除排序链表中的重复元素

    题目: 删除排序链表中的重复元素 给定一个排序链表,删除所有重复的元素每个元素只留下一个.   您在真实的面试中是否遇到过这个题? 样例 给出1->1->2->null,返回 1-& ...

  6. JavaWeb项目开发案例精粹-第2章投票系统-006view层

    1.index.jsp <%@ page language="java" import="java.util.*" pageEncoding=" ...

  7. React如何性能调优

    一. 二.调优例子 <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset=&q ...

  8. Excel多条件筛选求和

    单位A 代码B 面积(㎡)C A组 011 124 A组 123 15 A组 011 356 A组 123 44 B组 123 31 B组 011 2 B组 123 2 按照单位和代码求面积的和,可以 ...

  9. Java—反射

    通过程序化的方式间接对Class的对象实例操作,Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数.属性和方法 ...

  10. Win8-64位安装OpenSSL详细过程

    相关软件: 1.ActivePerl 5.22.1 : http://www.activestate.com/activeperl/downloads 2.Microsoft visual_studi ...