版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2018.08.29 |
前言
TextKit
框架是對Core Text
的封裝,用簡潔的調用方式實現了大部分Core Text
的功能。 TextKit是一個偏上層的開發框架,在iOS7
以上可用,使用它可以方便靈活處理復雜的文本布局,滿足開發中對文本布局的各種復雜需求。TextKit實際上是基于CoreText的一個上層框架,其是面向對象的。接下來幾篇我們就一起看一下這個框架。
簡介
注意:
TextKit
框架是對Core Text
的封裝,是iOS7以上才可以使用的框架,是文字排版和渲染引擎。
1. 幾種渲染控件
在iOS開發中,處理文本的視圖控件主要有4中,UILabel
,UITextField
,UITextView
和UIWebView
。其中UILabel與UITextField相對簡單,UITextView是功能完備的文本布局展示類,通過它可以進行復雜的富文本布局,UIWebView主要用來加載網頁或者pdf文件,其可以進行HTML
、CSS
和JS
等文件的解析。
2. 文字框架層級關系
在iOS 7之前的文字渲染框架層級主要如下所示:
在iOS 7之后,文字渲染框架的層級主要如下所示:
3. 包含的主要的類和關系
下面我們就看一下該框架包含的類及其關系,如下所示:
-
NSTextStorage:是
NSMutableAttributedString
的子類,負責存儲需要處理的文本及其屬性。- 1)
NSTextStorage
集成父類的所有的屬性,但NSTextStorage
包含了一個方法,可以將所有對其內容進行的修改以通知的方式發送出來。 - 2)使用一個自定義的
NSTextStorage
就可以讓文本在稍后動態地添加字體或者顏色高亮等文本屬性修飾。
- 1)
-
NSLayoutManager:負責將
NSTextStorage
中的文本數據渲染到顯示區域上,負責字符的編碼和布局。他管理著每一個字符的顯示。- 1)與
NSTextStorage
的關系:它監聽著NSTextStorage
發出的關于string
屬性改變的通知,一旦接受到通知就會觸發重新布局。 - 2)從
NSTextStorage
中獲取string
(內容)將其轉化為字形(與當前設置的字體等內容相關)。 - 3)一旦字形完全生成完畢,
NSLayoutManager
(管理者)會向NSTextContainer
查詢文本可用的繪制區域。 - 4)
NSTextContainer
會將文本的當前狀態改為無效,然后交給textView
去顯示。
- 1)與
- NSTextContainer:描述了一個顯示區域,默認是矩形,其子類可以定義任意的形狀。它不僅定義了可填充的區域,而且內部還定義了一個不可填充區域(Bezier Path 數組)。
數據流程如下圖所示:
這個類的關系如下所示:
通常情況下,一個NSTextStorage
對應 一個NSLayoutManager
對應 一個 NSTextContainer
。
當文字顯示為多列、多頁時,一個NSLayoutManager
對應多個 NSTextContainer
。
當采用不同的排版方式時,一個NSTextStorage
對應多個NSLayoutManager
。
通常由
NSLayoutManager
從NSTextStorage
中讀取出文本數據,然后根據一定的排版方式,將文本排版到NSTextContainer
中,再由NSTextContainer
結合UITextView
將最終效果顯示出來。
4. 進行文本布局渲染的流程
下面我們就主要看一下用TextKit進行文本布局流程。
使用TextKit進行文本的布局展示十分繁瑣,首先需要將顯示內容定義為一個NSTextStorage
對象,之后為其添加一個布局管理器對象NSLayoutManager
,在NSLayoutManager
中,需要進行NSTextContainer
的定義,定義多了NSTextContainer
對象則會將文本進行分頁。最后,將要展示的NSTextContainer
綁定到具體的UITextView
視圖上。
5. 幾種常見功能
下面看一下TextKit
可以實現的功能:
- 字距調整
(Kerning)
- 連寫
- 圖像附件:可以向
TextView
里添加圖像了 - 斷字:設置
hyphenationFactor
就可以啟用斷字 - 可定制性
- 更多的富文本屬性:設置不同的下劃線,雙線,粗線,虛線,點線或者他們的組合。
- 序列化
- 文本樣式:全局預定義文本類型
- 文本效果:使用這個效果的文本看起來就像蓋紙上面一樣。
后面會進行詳細的示例和說明。
幾中應用場景和簡單示例
下面我們就簡單的看一下幾種應用場景和簡單示例。這里由于篇幅的限制可能一篇不能完成,那么會在多篇中逐步完善。
1. UILabel不同文字的點擊
大家都知道UILabel是可以進行交互的,那需要打開UserInterfaceEnabled
,因為默認的交互是關著的。但是即使打開的話我們添加手勢,如果不判斷點擊區域的話,那么接收手勢響應的就是整個UILabel控件,但是利用TextKit框架就可以實現UILabel不同區域文字的點擊。
下面首先還是先看代碼,主要是兩個類 —— JJLabel
和JJLabelVC
。
JJLabel
JJLabelVC
下面就看一下源碼。
1. JJLabelVC.h
import <UIKit/UIKit.h>
@interface JJLabelVC : UIViewController
@end
2. JJLabelVC.m
#import "JJLabelVC.h"
#import "JJLabel.h"
@interface JJLabelVC ()
@property (nonatomic, strong) JJLabel *label;
@property (nonatomic, strong) UILabel *infoLabel;
@end
@implementation JJLabelVC
#pragma mark - Override Base Function
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.frame = [UIScreen mainScreen].bounds;
self.view.backgroundColor = [UIColor lightGrayColor];
[self initUI];
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.label.frame = CGRectMake(50, 100, 300, 40);
self.infoLabel.frame = CGRectMake(50, 400, 300, 40);
}
#pragma mark - Object Private Function
- (void)initUI
{
//詳情label
self.infoLabel = [[UILabel alloc] init];
self.infoLabel.backgroundColor = [UIColor magentaColor];
self.infoLabel.textAlignment = NSTextAlignmentCenter;
[self.view addSubview:self.infoLabel];
//展示label
self.label = [[JJLabel alloc] init];
self.label.textAlignment = NSTextAlignmentCenter;
self.label.backgroundColor = [UIColor greenColor];
NSMutableAttributedString *attributedString1 = [[NSMutableAttributedString alloc] initWithString:@"讓我們"];
[attributedString1 addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(0, attributedString1.length)];
[self.label appendString:attributedString1 block:^(NSAttributedString *attributeString) {
self.infoLabel.text = @"讓我們";
NSLog(@"%@", attributedString1);
}];
NSMutableAttributedString *attributedString2 = [[NSMutableAttributedString alloc] initWithString:@"蕩起"];
[attributedString2 addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, attributedString2.length)];
[self.label appendString:attributedString2 block:^(NSAttributedString *attributeString) {
self.infoLabel.text = @"蕩起";
NSLog(@"%@", attributedString2);
}];
NSMutableAttributedString *attributedString3 = [[NSMutableAttributedString alloc] initWithString:@"雙槳"];
[attributedString3 addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(0, attributedString3.length)];
[self.label appendString:attributedString3 block:^(NSAttributedString *attributeString) {
self.infoLabel.text = @"雙槳";
NSLog(@"%@", attributedString3);
}];
[self.view addSubview:self.label];
}
@end
3. JJLabel.h
#import <UIKit/UIKit.h>
typedef void(^textKitStringBlock)(NSAttributedString *attributeString);
@interface JJLabel : UILabel
//字符串更新并添加回調
- (void)appendString:(NSAttributedString *)attributeString block:(textKitStringBlock)block;
@end
4. JJLabel.m
#import "JJLabel.h"
@interface JJLabel()
@property (nonatomic, strong) NSMutableArray <NSAttributedString *> *subAttributedStringsArrM;
@property (nonatomic, strong) NSMutableArray <NSValue *> *subAttributedStringRangesArrM;
@property (nonatomic, strong) NSMutableArray <textKitStringBlock> *stringOptionsArrM;
@property (nonatomic, strong) NSTextStorage *textStorage;
@property (nonatomic, strong) NSLayoutManager *layoutManager;
@property (nonatomic, strong) NSTextContainer *textContainer;
@end
@implementation JJLabel
#pragma mark - Override Base Function
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.userInteractionEnabled = YES;
self.subAttributedStringsArrM = [NSMutableArray array];
self.subAttributedStringRangesArrM = [NSMutableArray array];
self.stringOptionsArrM = [NSMutableArray array];
[self setupSystemTextKitConfiguration];
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.textContainer.size = self.bounds.size;
}
- (void)drawTextInRect:(CGRect)rect
{
NSRange range = NSMakeRange(0, self.textStorage.length);
[self.layoutManager drawBackgroundForGlyphRange:range atPoint:CGPointMake(0.0, 0.0)];
[self.layoutManager drawGlyphsForGlyphRange:range atPoint:CGPointMake(0.0, 0.0)];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = touches.anyObject;
CGPoint point = [touch locationInView:self];
//根據點來獲取該位置glyph的index
NSInteger glythIndex = [self.layoutManager glyphIndexForPoint:point inTextContainer:self.textContainer];
//獲取改glyph對應的rect
CGRect glythRect = [self.layoutManager boundingRectForGlyphRange:NSMakeRange(glythIndex, 1) inTextContainer:self.textContainer];
//最終判斷該字形的顯示范圍是否包括點擊的location
if (CGRectContainsPoint(glythRect, point)) {
NSInteger characterIndex = [self.layoutManager characterIndexForGlyphAtIndex:glythIndex];
[self.subAttributedStringRangesArrM enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSRange range = obj.rangeValue;
if (NSLocationInRange(characterIndex, range)) {
textKitStringBlock block = self.stringOptionsArrM[idx];
block(self.subAttributedStringsArrM[idx]);
}
}];
}
}
#pragma mark - Object Private Function
- (void)setupSystemTextKitConfiguration
{
self.textStorage = [[NSTextStorage alloc] init];
self.layoutManager = [[NSLayoutManager alloc] init];
self.textContainer = [[NSTextContainer alloc] init];
[self.textStorage addLayoutManager:self.layoutManager];
[self.layoutManager addTextContainer:self.textContainer];
}
#pragma mark - Object Public Function
//字符串更新并添加回調
- (void)appendString:(NSAttributedString *)attributeString block:(textKitStringBlock)block
{
[self.subAttributedStringsArrM addObject:attributeString];
NSRange range = NSMakeRange(self.textStorage.length, attributeString.length);
[self.subAttributedStringRangesArrM addObject:[NSValue valueWithRange:range]];
[self.stringOptionsArrM addObject:block];
[self.textStorage appendAttributedString:attributeString];
}
#pragma mark - Getter && Setter
- (void)setText:(NSString *)text
{
[super setText:text];
[self setupSystemTextKitConfiguration];
}
- (void)setAttributedText:(NSAttributedString *)attributedText
{
[super setAttributedText:attributedText];
[self setupSystemTextKitConfiguration];
}
@end
下面看一下實現效果
下面看一下控制臺輸出:
2018-08-28 14:47:37.224444+0800 JJTextKit[16504:511572] 讓我們{
NSColor = "UIExtendedSRGBColorSpace 0 0 1 1";
}
2018-08-28 14:47:38.712045+0800 JJTextKit[16504:511572] 蕩起{
NSColor = "UIExtendedSRGBColorSpace 1 0 0 1";
}
2018-08-28 14:47:39.727875+0800 JJTextKit[16504:511572] 雙槳{
NSColor = "UIExtendedSRGBColorSpace 0 0 1 1";
}
2018-08-28 14:47:40.959816+0800 JJTextKit[16504:511572] 蕩起{
NSColor = "UIExtendedSRGBColorSpace 1 0 0 1";
}
2018-08-28 14:47:41.703850+0800 JJTextKit[16504:511572] 讓我們{
NSColor = "UIExtendedSRGBColorSpace 0 0 1 1";
}
2. 部分文字的高亮等特殊顯示
對于這個問題,如果你看過我寫的前面幾篇文章,那么可以在那里找到答案,這里只給出鏈接不做具體說明了。
接下來我們要做的是用另外一種方法進行實現。
還是先看一下代碼:
1. JJHighlightVC.h
#import <UIKit/UIKit.h>
@interface JJHighlightVC : UIViewController
@end
2. JJHighlightVC.m
#import "JJHighlightVC.h"
#import "JJHighlightStorage.h"
@interface JJHighlightVC ()
@property (nonatomic, strong) UITextView *textView;
@property (nonatomic, strong) JJHighlightStorage *textStorage;
@property (nonatomic, strong) NSTextContainer *textContainer;
@property (nonatomic, strong) NSLayoutManager *layoutManager;
@end
@implementation JJHighlightVC
#pragma mark - Override Base Function
- (void)viewDidLoad
{
[super viewDidLoad];
self.textContainer = [[NSTextContainer alloc] init];
self.layoutManager = [[NSLayoutManager alloc] init];
self.textStorage = [[JJHighlightStorage alloc] init];
[self.textStorage addLayoutManager:self.layoutManager];
[self.layoutManager addTextContainer:self.textContainer];
self.textView = [[UITextView alloc] initWithFrame:CGRectMake(10.0, 250.0, self.view.bounds.size.width - 20.0, 200) textContainer:self.textContainer];
self.textView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:self.textView];
[self.textStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:@"波浪線引起來的字符都會被~變為藍色~,對,是這樣的"];
}
@end
3. JJHighlightStorage.h
#import <UIKit/UIKit.h>
@interface JJHighlightStorage : NSTextStorage
@end
4. JJHighlightStorage.m
#import "JJHighlightStorage.h"
@interface JJHighlightStorage()
@property (nonatomic, strong) NSMutableAttributedString *mutableAttributedString;
@property (nonatomic, strong) NSRegularExpression *expression;
@end
@implementation JJHighlightStorage
#pragma mark - Override Base Function
- (instancetype)init
{
self = [super init];
if (self) {
self.mutableAttributedString = [[NSMutableAttributedString alloc] init];
self.expression = [NSRegularExpression regularExpressionWithPattern:@"(\\~\\w+(\\s*\\w+)*\\s*\\~)" options:0 error:NULL];
}
return self;
}
- (NSString *)string
{
return self.mutableAttributedString.string;
}
- (NSDictionary<NSString *,id> *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
return [self.mutableAttributedString attributesAtIndex:location effectiveRange:range];
}
- (void)setAttributes:(NSDictionary<NSString *,id> *)attrs range:(NSRange)range
{
[self beginEditing];
[self.mutableAttributedString setAttributes:attrs range:range];
[self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
[self endEditing];
}
// Sends out -textStorage:willProcessEditing, fixes the attributes, sends out -textStorage:didProcessEditing, and notifies the layout managers of change with the -processEditingForTextStorage:edited:range:changeInLength:invalidatedRange: method. Invoked from -edited:range:changeInLength: or -endEditing.
- (void)processEditing
{
[super processEditing];
//去除當前段落的顏色屬性
NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];
[self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];
//根據正則匹配,添加新屬性
[self.expression enumerateMatchesInString:self.string options:NSMatchingReportProgress range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
[self addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:result.range];
}];
}
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
[self beginEditing];
[self.mutableAttributedString replaceCharactersInRange:range withString:str];
[self edited:NSTextStorageEditedCharacters range:range changeInLength:(NSInteger)str.length - (NSInteger)range.length];
[self endEditing];
}
@end
下面看一下效果
參考文章
1. 最詳細TextKit分析
2. TextKit框架
3. TextKit功能和結構
4. TextKit介紹(轉載3篇文章)
5. TextKit詳解
6. 學習TextKit框架(上)
7. iOS Text Part1:TextKit
8. TextKit 探究
后記
本篇主要講述了TextKit框架基本概覽和兩個常用的使用場景,感興趣的給個贊或者關注~~~