简介:
iOS7 的发布给开发者的案头带来了很多新工具。其中一个就是 TextKit(文本工具箱)。TextKit 由许多新的 UIKit 类组成,顾名思义,这些类就是用来处理文本的。
1.NSTextStorage 专门用于存储内容的
2.NSLayoutManager 专门用于管理布局
3.NSTextContainer 专门用于指定绘制的区域
字符串(String):要绘制文本,那么必然在某个地方有个字符串存储它。在默认的结构中,NSTextStorage 保存并管理这个字符串,在这种情况中,它可以远离绘制。但并不一定非得这样。使用 TextKit 时,文本可以来自任何适合的来源。例如,对于一个代码编辑器,字符串可以是一棵包含所有显示的代码的结构信息的注释语法树(annotated syntax tree, AST)。使用一个定制的文本存储,这个文本只在后面动态地添加字体或颜色高亮等文本属性装饰。这是第一次,开发者可以直接为文本组件使用自己的模型。只需要一个特别设计的文本存储。即:
NSTextStorage:如果你把文本系统看做一个模型-视图-控制器(MVC)架构,这个类代表的是模型。文本存储是中心对象,它知道所有的文本和属性信息。它只提供了两个存取器方法存取它们,并提供了另外两个方法来修改它们。后面我们将进一步了解它们。现在重要的是你得理解 NSTextStorage 是从它的父类 NSAttributedString 继承了这些方法。这就很清楚了,文本存储——从文本系统看来——仅仅是一个带有属性的字符串,以及几个扩展。这两者唯一的重大不同点是文本存储包含了一个方法来发送内容改变的通知。我们会马上介绍这部分内容。
UITextView:堆栈的另一头是实际的视图。在 TextKit 中,文本视图有两个目的:第一,它是文本系统用来绘制的视图。文本视图它自己并不会做任何绘制;它仅仅提供一个供其它类绘制的区域。作为视图层级机构中唯一的组件,第二个目的是处理所有的用户交互。具体来说,文本视图实现 UITextInput 的协议来处理键盘事件,它为用户提供了一种途径来设置一个插入点或选择文本。它并不对文本做任何实际上的改变,仅仅将这些改变请求转发给刚刚讨论的文本存储。
NSTextContainer:每个文本视图定义了一个文本可以绘制的区域。为此,每个文本视图都有一个文本容器,它精确地描述了这个可用的区域。在简单的情况下,这是一个垂直的无限相当大的矩形区域。文本被填充到这个区域,并且文本视图允许用户滚动它。然而,在更高级的情况下,这个区域可能是一个无限大的矩形。例如,当渲染一本书时,每一页都有最大的高度和宽度。文本容器会定义这个大小,并且不接受任何超出的文本。相同情况下,一幅图像可能占据了页面的一部分,文本应该沿着它的边缘重新排版。这也是由文本容器来处理的,我们会在后面的例子中看到这一点。
NSLayoutManager:布局管理器是中心组件,它把所有组件粘合在一起:
- 1、这个管理器监听文本存储中文本或属性改变的通知,一旦接收到通知就触发布局进程。
- 2、从文本存储提供的文本开始,它将所有的字符翻译为字形(Glyph)(附注2).
- 3、一旦字形全部生成,这个管理器向它的文本容器(们)查询文本可用以绘制的区域
- 4、然后这些区域被行逐步填充,而行又被字形逐步填充。一旦一行填充完毕,下一行开始填充。
- 5、对于每一行,布局管理器必须考虑断行行为(放不下的单词必须移到下一行)、连字符、内联的图像附件等等。
- 6、当布局完成,文本的当前显示状态被设为无效,然后文本管理器将前面几步排版好的文本设给文本视图。
CoreText:没有直接包含在 TextKit 中,CoreText 是进行实际排版的库。对于布局管理器的每一步,CoreText 被这样或那样的方式调用。它提供了从字符到字形的翻译,用它们来填充行,以及建议断字点。
使用示例:
customLabel.text = "百度一下你就知道 http://www.baidu.com"
代码示例:
import UIKit class JQLabel: UILabel { override var text: String? { didSet{ // 1.修改textStorage存储的内容 textStorage.setAttributedString(NSAttributedString(string: text!)) // 2.设置textStorage的属性 textStorage.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(20), range: NSMakeRange(0, text!.characters.count)) // 3.处理URL self.URLRegex() // 2.通知layoutManager重新布局 setNeedsDisplay() } } // 如果是UILabel调用setNeedsDisplay方法, 系统会促发drawTextInRect override func drawTextInRect(rect: CGRect) { // 重绘 // 字形 ; 理解为一个小的UIView /* 第一个参数: 指定绘制的范围 第二个参数: 指定从什么位置开始绘制 */ layoutManager.drawGlyphsForGlyphRange(NSMakeRange(0, text!.characters.count), atPoint: CGPointZero) } override init(frame: CGRect) { super.init(frame: frame) setupSystem() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupSystem() } private func setupSystem() { // 1.将layoutManager添加到textStorage textStorage.addLayoutManager(layoutManager) // 2.将textContainer添加到layoutManager layoutManager.addTextContainer(textContainer) } override func layoutSubviews() { super.layoutSubviews() // 3.指定区域 textContainer.size = bounds.size } // MARK: -懒加载 /* 只要textStorage中的内容发生变化, 就可以通知layoutManager重新布局 layoutManager重新布局需要知道绘制到什么地方, 所以layoutManager就会文textContainer绘制的区域 */ // 专门用于存储内容的 // textStorage 中有 layoutManager private lazy var textStorage = NSTextStorage() // 专门用于管理布局 // layoutManager 中有 textContainer private lazy var layoutManager = NSLayoutManager() // 专门用于指定绘制的区域 private lazy var textContainer = NSTextContainer() func URLRegex() { // 1.创建一个正则表达式对象 do{ let dataDetector = try NSDataDetector(types: NSTextCheckingTypes(NSTextCheckingType.Link.rawValue)) let res = dataDetector.matchesInString(textStorage.string, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, textStorage.string.characters.count)) // 4取出结果 for checkingRes in res { let str = (textStorage.string as NSString).substringWithRange(checkingRes.range) let tempStr = NSMutableAttributedString(string: str) // tempStr.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, str.characters.count)) tempStr.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(20), NSForegroundColorAttributeName: UIColor.redColor()], range: NSMakeRange(0, str.characters.count)) textStorage.replaceCharactersInRange(checkingRes.range, withAttributedString: tempStr) } }catch { print(error) } } }
点击事件的处理:
import UIKit class JQTextView: UITextView { override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { // 1.获取手指点击的位置 let touch = (touches as NSSet).anyObject()! let point = touch.locationInView(touch.view) print(point) // 2.获取URL的区域 // 注意: 没有办法直接设置UITextRange的范围 let range = NSMakeRange(10, 20) // 只要设置selectedRange, 那么就相当于设置了selectedTextRange selectedRange = range // 给定指定的range, 返回range对应的字符串的rect // 返回数组的原因是因为文字可能换行 let array = selectionRectsForRange(selectedTextRange!) for selectionRect in array{ // let tempView = UIView(frame: selectionRect.rect) // tempView.backgroundColor = UIColor.redColor() // addSubview(tempView) if CGRectContainsPoint(selectionRect.rect, point) { print("点击了URL") } } } }
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:touch.view]; CGPoint pp = [self convertPoint:point toView:self.textView]; GPLink *touchingLink = [self touchingLinkWithPoint:pp]; if (touchingLink) { [[NSNotificationCenter defaultCenter] postNotificationName:GPLinkDidSelectedNotification object:nil userInfo:@{GPLinkText : touchingLink.text}]; } [self touchesCancelled:touches withEvent:event]; } - (GPLink *)touchingLinkWithPoint:(CGPoint)point { __block GPLink *touchingLink = nil; [self.links enumerateObjectsUsingBlock:^(GPLink *link, NSUInteger idx, BOOL *stop) { for (UITextSelectionRect *selectionRect in link.rects) { if (CGRectContainsPoint(selectionRect.rect, point)) { NSLog(@"选中%@",NSStringFromCGRect(selectionRect.rect)); touchingLink = link; break; } } }]; return touchingLink; }