在上一篇文章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)设定字体:

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就可以了。

NSDictionary *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可以动态的根据用户设置的字体大小进行调整,但是如果具体某个文本显示控件中的文本样式能够动态调整是不是会更酷一些呢?

例如,你希望让你的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,在实现文件中添加一个成员变量:

#import "MarkupTextStorage.h"

@implementation MarkupTextStorage
{
NSMutableAttributedString *_backingStore;
} - (id)init
{
self = [super init];
if (self) {
_backingStore = [[NSMutableAttributedString alloc] init];
}
return self;
} @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中对抽象工厂模式的简单实现,为创建一些列相关或独立对象提供了统一的接口而不用指定具体的类。常用的像NSArrayNSNumber事实上也是一系列类集群的开放接口。

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

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

好了,上面解释了这么多其实主要就说明了为什么子类化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]];
[super processEditing];
}

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* key in _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)];
}
}];
}
}

在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. 从一次面试经历谈PHP的普通传值与引用传值以及unset

    关于这个概念一般都会在PHP的第一堂课说变量的时候给介绍,并且我以前还给其他PHPer介绍这个概念.但是作为一个工作一段时间的PHPer的我,竟然在面试的时候一下子拿不定主意最后还答错了,很觉得丢脸( ...

  2. PowerDesigner生成的ORACLE 建表脚本中去掉对象的双引号,设置大、小写

    原文:PowerDesigner生成的ORACLE 建表脚本中去掉对象的双引号,设置大.小写 若要将 CDM 中将 Entity的标识符都设为指定的大小写,则可以这么设定: 打开cdm的情况下,进入T ...

  3. [iOS]开发者证书和描述文件的作用

    先说下证书吧. 然后是描述文件

  4. ORA-12571 : TNS : 包写入程序失败

    错误原因 解决方案 修改D:/oracle/ora92/network/admin目录下sqlnet.ora,将”NAMES.DEFAULT_DOMAIN =” 这一行用#注释掉,将“SQLNET.A ...

  5. Spring 操作数据库

    试了一下spring的JdbcTemplate觉得很好用.首先增加一个连接到mysql数据库的dataSource <bean id="dataSource2" class= ...

  6. How to learn linux device driver

    To learn device driver development, like any other new knowledge, the bestapproach for me is to lear ...

  7. vs2012 arcgis engine 10 丢失arcgis模板

    1.Visual Studio 2012环境下安装ArcGIS Engine 10 Visual Studio 2012环境下安装ArcObject SDK for the Microsoft .Ne ...

  8. kafka的环境搭建

    kafka是一个高吞吐量的消息系统.隔离消息接收和处理过程(可理解为一个缓存) 1.kafka伪分布的部署 1.1.下载并解压 1.2.启动zk bin/zookeeper-server-start. ...

  9. 一个简单的iBatis入门例子

    一个简单的iBatis入门例子,用ORACLE和Java测试 目录结构: 1.导入iBatis和oracle驱动. 2.创建类Person.java package com.ibeats;import ...

  10. codevs 1135 选择客栈

    这题没什么话说. #include<iostream> #include<cstdio> #include<cstring> #include<algorit ...