TextKit
来源:互联网 发布:js url encode 编辑:程序博客网 时间:2024/06/04 18:50
关注:http://www.jianshu.com/p/12a10e36d659
以前,如果我们想实现如上图所示复杂的文本排版:显示不同样式的文本、图片和文字混排,你可能就需要借助于UIWebView或者深入研究一下Core Text。在iOS6中,UILabel、UITextField、UITextView增加了一个NSAttributedString属性,可以稍微解决一些排版问题,但是支持的力度还不够。现在Text Kit完全改变了这种现状。
1.NSAttributedString
下面的例子,展示如何label中显示属性化字符串:
-(void)setAttributeStringLabel{ NSString *str = @"bold,little color,hello"; //NSMutableAttributedString的初始化 NSDictionary *attrs = @{NSFontAttributeName:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]}; NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]initWithString:str attributes:attrs]; //NSMutableAttributedString增加属性 [attributedString addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:36] range:[str rangeOfString:@"bold"]]; [attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:[str rangeOfString:@"little color"]]; [attributedString addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"Papyrus" size:36] range:NSMakeRange(18,5)]; [attributedString addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:18] range:[str rangeOfString:@"little"]]; //NSMutableAttributedString移除属性 [attributedString removeAttribute:NSFontAttributeName range:[str rangeOfString:@"little"]]; //NSMutableAttributedString设置属性 NSDictionary *attrs2 = @{NSStrokeWidthAttributeName:@-5, NSStrokeColorAttributeName:[UIColor greenColor], NSFontAttributeName:[UIFont systemFontOfSize:36], NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle)}; [attributedString setAttributes:attrs2 range:NSMakeRange(0, 4)]; self.label.attributedText = attributedString;}
运行结果如下:
需要注意的是,你不能直接修改已有的AttributedString, 你需要把它copy出来,修改后再进行设置:
NSMutableAttributedString *labelText = [myLabel.attributedText mutableCopy]; [labelText setAttributes:...];myLabel.attributedText = labelText;
2.Dynamic type:动态字体
iOS7增加了一项用户偏好设置:动态字体,用户可以通过显示与亮度-文字大小设置面板来修改设备上所有字体的尺寸。为了支持这个特性,意味着不要用systemFontWithSize:,而要用新的字体选择器preferredFontForTextStyle:。iOS提供了六种样式:标题,正文,副标题,脚注,标题1,标题2。例如:
_textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
你可以接收用户改变字体大小的通知:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferredContentSizeChanged:) name:UIContentSizeCategoryDidChangeNotification object:nil];-(void)preferredContentSizeChanged:(NSNotification *)notification{ _textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];}
3.Exclusion paths:排除路径
iOS 上的 NSTextContainer 提供了exclusionPaths,它允许开发者设置一个 NSBezierPath 数组来指定不可填充文本的区域。如下图:
正如你所看到的,所有的文本都放置在蓝色椭圆外面。在 Text View 里面实现这个行为很简单,但是有个小麻烦:Bezier Path 的坐标必须使用容器的坐标系。以下是转换方法,将它的 bounds(self.circleView.bounds)转换到 Text View 的坐标系统:
- (void)updateExclusionPaths{ CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds fromView:self.circleView]; }
因为没有 inset,文本会过于靠近视图边界,所以 UITextView 会在离边界还有几个点的距离的地方插入它的文本容器。因此,要得到以容器坐标表示的路径,必须从 origin 中减去这个插入点的坐标。
ovalFrame.origin.x -= self.textView.textContainerInset.left;ovalFrame.origin.y -= self.textView.textContainerInset.top;
在此之后,只需将 Bezier Path 设置给 Text Container 即可将对应的区域排除掉。其它的过程对你来说是透明的,TextKit 会自动处理。
self.textView.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithOvalInRect: ovalFrame]];
4.多容器布局
NSTextStorage:它是NSMutableAttributedString的子类,里面存的是要管理的文本。
NSLayoutManager:管理文本布局方式
NSTextContainer:表示文本要填充的区域
如上图所示,它们的关系是 1 对 N 的关系。就是那样:一个 Text Storage 可以拥有多个 Layout Manager,一个 Layout Manager 也可以拥有多个 Text Container。这些多重性带来了多容器布局的特性:
1)将多个 Layout Manager 附加到同一个 Text Storage 上,可以产生相同文本的多种视觉表现,如果相应的 Text View 可编辑,那么在某个 Text View 上做的所有修改都会马上反映到所有 Text View 上。
NSTextStorage *sharedTextStorage = self.originalTextView.textStorage; [sharedTextStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:kstring]; // 将一个新的 Layout Manager 附加到上面的 Text Storage 上 NSLayoutManager *otherLayoutManager = [NSLayoutManager new]; [sharedTextStorage addLayoutManager: otherLayoutManager]; NSTextContainer *otherTextContainer = [NSTextContainer new]; [otherLayoutManager addTextContainer: otherTextContainer]; UITextView *otherTextView = [[UITextView alloc] initWithFrame:self.otherContainerView.bounds textContainer:otherTextContainer]; otherTextView.backgroundColor = self.otherContainerView.backgroundColor; otherTextView.translatesAutoresizingMaskIntoConstraints = YES; otherTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; otherTextView.scrollEnabled = NO; [self.otherContainerView addSubview: otherTextView]; self.otherTextView = otherTextView;
2)将多个 Text Container 附加到同一个 Layout Manager 上,这样可以将一个文本分布到多个视图展现出来。下面的例子将展示这两个特性:
// 将一个新的 Text Container 附加到同一个 Layout Manager,这样可以将一个文本分布到多个视图展现出来。 NSTextContainer *thirdTextContainer = [NSTextContainer new]; [otherLayoutManager addTextContainer: thirdTextContainer]; UITextView *thirdTextView = [[UITextView alloc] initWithFrame:self.thirdContainerView.bounds textContainer:thirdTextContainer]; thirdTextView.backgroundColor = self.thirdContainerView.backgroundColor; thirdTextView.translatesAutoresizingMaskIntoConstraints = YES; thirdTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self.thirdContainerView addSubview: thirdTextView]; self.thirdTextView = thirdTextView;
结果如下所示:
5.语法高亮:继承NSTextStorage
看看 TextKit 组件的责任划分,就很清楚语法高亮应该由 Text Storage 实现。不过NSTextStorage 不是一个普通的类,它是一个类簇,你可以把它理解为一个"半具体"子类,因此要继承它必须实现以下方法:
- string;- attributesAtIndex:effectiveRange:- replaceCharactersInRange:withString:- setAttributes:range:
我们新建一个NSTextStorage的子类:SyntaxHighlightTextStorage
要实现以上4个方法,我们首先需要通过NSMutableAttributedString 实现一个后备存储,- setAttributes:range:这个方法需要用beginEditing和endEditing包起来,而且必须调用 edited:range:changeInLength:,所以大部分的NSTextStorage的子类都长下面这个样子:
@implementation SyntaxHighlightTextStorage{ NSMutableAttributedString *_backingStore;}- (instancetype)init{ self = [super init]; if (self) { _backingStore = [NSMutableAttributedString new]; } return self;}//1- (NSString *)string { return [_backingStore string];}//2- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range{ return [_backingStore attributesAtIndex:location effectiveRange:range];}//3- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str{ NSLog(@"replaceCharactersInRange:%@ withString:%@",NSStringFromRange(range), str); [self beginEditing]; [_backingStore replaceCharactersInRange:range withString:str]; [self edited:NSTextStorageEditedCharacters | NSTextStorageEditedAttributes range:range changeInLength:str.length - range.length]; [self endEditing];}//4- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range { NSLog(@"setAttributes:%@ range:%@", attrs, NSStringFromRange(range)); [self beginEditing]; [_backingStore setAttributes:attrs range:range]; [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; [self endEditing];}
一个方便实现高亮的办法是覆盖 -processEditing,并设置一个正则表达式来查找单词,每次文本存储有修改时,这个方法都自动被调用。
- (void)processEditing{ [super processEditing]; static NSRegularExpression *expression; expression = expression ?: [NSRegularExpression regularExpressionWithPattern:@"(\\*\\w+(\\s\\w+)*\\*)\\s" options:0 error:NULL]; }
首先清除之前所有的高亮:
NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange]; [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];
其次遍历所有的样式匹配项并高亮它们:
[expression enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { [self addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:result.range]; }];
就这样,我们在文本系统栈里面有了一个 Text Storage 的全功能替换版本。在从 Interface 文件中载入时,可以像这样将它插入文本视图:
- (void)createTextView { _textStorage = [SyntaxHighlightTextStorage new]; [_textStorage addLayoutManager: self.textView.layoutManager]; [_textStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:@"在从 Interface 文件中载入时,可以像这样将它插入文本视图,然后加 *星号* 的字就会高亮出来了"]; _textView.delegate = self;}
运行如下:
6.文本容器修改:继承NSTextContainer
通过继承NSTextContainer,我们可以使得textView不再是一个规规矩矩的矩形。NSTextContainer负责回答这个问题:对于给定的矩形,哪个部分可以放文字,这个问题由下面这个方法来回答:
- (CGRect)lineFragmentRectForProposedRect: atIndex: writingDirection: remainingRect:
所以我们在继承NSTextContainer的类中覆盖这个方法即可:
下面这个方法返回一个圆形区域:
- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(CGRect *)remainingRect { CGRect rect = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect]; CGSize size = [self size]; CGFloat radius = fmin(size.width, size.height) / 2.0; CGFloat ypos = fabs((proposedRect.origin.y + proposedRect.size.height / 2.0) - radius); CGFloat width = (ypos < radius) ? 2.0 * sqrt(radius * radius - ypos * ypos) : 0.0; CGRect circleRect = CGRectMake(radius - width / 2.0, proposedRect.origin.y, width, proposedRect.size.height); return CGRectIntersection(rect, circleRect);}
使用这个继承类:
- (void)viewDidLoad { [super viewDidLoad]; NSString *path = [[NSBundle mainBundle] pathForResource:@"sample.txt" ofType:nil]; NSString *string = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; NSMutableParagraphStyle *style = [NSMutableParagraphStyle new]; [style setAlignment:NSTextAlignmentJustified]; NSTextStorage *text = [[NSTextStorage alloc] initWithString:string attributes:@{ NSParagraphStyleAttributeName: style, NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleCaption2] }]; NSLayoutManager *layoutManager = [NSLayoutManager new]; [text addLayoutManager:layoutManager]; CGRect textViewFrame = CGRectMake(20, 20, 280, 280); CircleTextContainer *textContainer = [[CircleTextContainer alloc] initWithSize:textViewFrame.size]; [textContainer setExclusionPaths:@[ [UIBezierPath bezierPathWithOvalInRect:CGRectMake(80, 120, 50, 50)]]]; [layoutManager addTextContainer:textContainer]; UITextView *textView = [[UITextView alloc] initWithFrame:textViewFrame textContainer:textContainer]; textView.allowsEditingTextAttributes = YES; textView.scrollEnabled = NO; textView.editable = NO; [self.view addSubview:textView];}
效果如下:
7.布局修改:继承NSLayoutManager
利用NSLayoutManager的代理方法,我们可以轻松的设置行高:
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect{ return floorf(glyphIndex / 100);}
假设你的文本中有链接,你不希望这些链接被断行分割。如果可能的话,一个 URL 应该始终显示为一个整体,一个单一的文本片段。没有什么比这更简单的了。
首先,就像前面讨论过的那样,我们使用自定义的 Text Storage,如下:
static NSDataDetector *linkDetector;linkDetector = linkDetector ?: [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:NULL];NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)];[self removeAttribute:NSLinkAttributeName range:paragaphRange];[linkDetector enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop){ [self addAttribute:NSLinkAttributeName value:result.URL range:result.range];}];
改变断行行为就只需要实现一个 Layout Manager 的代理方法:
- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex{ NSRange range; NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName atIndex:charIndex effectiveRange:&range]; return !(linkURL && charIndex > range.location && charIndex <= NSMaxRange(range));
结果就像下面这样:
你可以在这里下载到本文的代码。
参考:初识 TextKit,iOS 7 by Tutorials,iOS 7 Programming Pushing the Limits
原文链接:http://www.jianshu.com/p/2f72a5fa99f1
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
- Textkit
- TextKit
- TextKit浅析
- 认识 TextKit
- iOS-------------TextKit
- iOS-TextKit
- TextKit探秘
- 认识 TextKit
- TextKit YYText
- 初识 TextKit
- TextKit探究
- IOS7开发~TextKit学习
- TextKit学习NSTextStorage,NSLayoutManager,
- iOS 7 认识 TextKit
- 文本排版之----TextKit
- TextKit的使用
- IOS7 textkit 的相关
- ios-TextKit框架基础
- Struts1框架轻易入门,经典示例
- Text Kit学习(入门和进阶)
- scala进阶17-隐式类/方法-增强类功能
- Android Service学习之本地服务
- 2016.08.17
- TextKit
- HDOJ-----2063过山车(二分图)vector
- table表格td 内容自动换行
- ViewPagerIndicator(demo)
- ——廖一梅《像我这样笨拙地生活》经典语录
- 【分组背包变形】HDU3033-I love sneakers!
- openwrt开发教程之下载配置编译openwrt(MT7621A)
- zookeeper
- 51NOD 1035 最长的循环节