以前,如果我們想實(shí)現(xiàn)如上圖所示復(fù)雜的文本排版:顯示不同樣式的文本咱圆、圖片和文字混排循狰,你可能就需要借助于UIWebView或者深入研究一下Core Text榛做。在iOS6中,UILabel蚕苇、UITextField哩掺、UITextView增加了一個(gè)NSAttributedString屬性,可以稍微解決一些排版問題涩笤,但是支持的力度還不夠〗劳蹋現(xiàn)在Text Kit完全改變了這種現(xiàn)狀。
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設(shè)置屬性
NSDictionary *attrs2 = @{NSStrokeWidthAttributeName:@-5,
NSStrokeColorAttributeName:[UIColor greenColor],
NSFontAttributeName:[UIFont systemFontOfSize:36],
NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle)};
[attributedString setAttributes:attrs2 range:NSMakeRange(0, 4)];
self.label.attributedText = attributedString;
}
運(yùn)行結(jié)果如下:
需要注意的是恩沽,你不能直接修改已有的AttributedString, 你需要把它c(diǎn)opy出來誊稚,修改后再進(jìn)行設(shè)置:
NSMutableAttributedString *labelText = [myLabel.attributedText mutableCopy];
[labelText setAttributes:...];
myLabel.attributedText = labelText;
2.Dynamic type:動(dòng)態(tài)字體
iOS7增加了一項(xiàng)用戶偏好設(shè)置:動(dòng)態(tài)字體,用戶可以通過顯示與亮度-文字大小設(shè)置面板來修改設(shè)備上所有字體的尺寸罗心。為了支持這個(gè)特性里伯,意味著不要用systemFontWithSize:,而要用新的字體選擇器preferredFontForTextStyle:。iOS提供了六種樣式:標(biāo)題渤闷,正文疾瓮,副標(biāo)題,腳注飒箭,標(biāo)題1狼电,標(biāo)題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,它允許開發(fā)者設(shè)置一個(gè) NSBezierPath 數(shù)組來指定不可填充文本的區(qū)域肩碟。如下圖:
正如你所看到的卷员,所有的文本都放置在藍(lán)色橢圓外面。在 Text View 里面實(shí)現(xiàn)這個(gè)行為很簡單腾务,但是有個(gè)小麻煩:Bezier Path 的坐標(biāo)必須使用容器的坐標(biāo)系。以下是轉(zhuǎn)換方法,將它的 bounds(self.circleView.bounds)轉(zhuǎn)換到 Text View 的坐標(biāo)系統(tǒng):
- (void)updateExclusionPaths
{
CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds fromView:self.circleView];
}
因?yàn)闆]有 inset削饵,文本會過于靠近視圖邊界岩瘦,所以 UITextView 會在離邊界還有幾個(gè)點(diǎn)的距離的地方插入它的文本容器。因此窿撬,要得到以容器坐標(biāo)表示的路徑启昧,必須從 origin 中減去這個(gè)插入點(diǎn)的坐標(biāo)。
ovalFrame.origin.x -= self.textView.textContainerInset.left;
ovalFrame.origin.y -= self.textView.textContainerInset.top;
在此之后劈伴,只需將 Bezier Path 設(shè)置給 Text Container 即可將對應(yīng)的區(qū)域排除掉密末。其它的過程對你來說是透明的,TextKit 會自動(dòng)處理跛璧。
self.textView.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithOvalInRect: ovalFrame]];
4.多容器布局
NSTextStorage:它是NSMutableAttributedString的子類严里,里面存的是要管理的文本。
NSLayoutManager:管理文本布局方式
NSTextContainer:表示文本要填充的區(qū)域
如上圖所示追城,它們的關(guān)系是 1 對 N 的關(guān)系刹碾。就是那樣:一個(gè) Text Storage 可以擁有多個(gè) Layout Manager,一個(gè) Layout Manager 也可以擁有多個(gè) Text Container座柱。這些多重性帶來了多容器布局的特性:
1)將多個(gè) Layout Manager 附加到同一個(gè) Text Storage 上迷帜,可以產(chǎn)生相同文本的多種視覺表現(xiàn),如果相應(yīng)的 Text View 可編輯色洞,那么在某個(gè) Text View 上做的所有修改都會馬上反映到所有 Text View 上戏锹。
NSTextStorage *sharedTextStorage = self.originalTextView.textStorage;
[sharedTextStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:kstring];
// 將一個(gè)新的 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)將多個(gè) Text Container 附加到同一個(gè) Layout Manager 上,這樣可以將一個(gè)文本分布到多個(gè)視圖展現(xiàn)出來火诸。下面的例子將展示這兩個(gè)特性:
// 將一個(gè)新的 Text Container 附加到同一個(gè) Layout Manager锦针,這樣可以將一個(gè)文本分布到多個(gè)視圖展現(xiàn)出來。
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;
結(jié)果如下所示:
5.語法高亮:繼承NSTextStorage
看看 TextKit 組件的責(zé)任劃分惭蹂,就很清楚語法高亮應(yīng)該由 Text Storage 實(shí)現(xiàn)伞插。不過NSTextStorage 不是一個(gè)普通的類,它是一個(gè)類簇盾碗,你可以把它理解為一個(gè)"半具體"子類媚污,因此要繼承它必須實(shí)現(xiàn)以下方法:
- string;
- attributesAtIndex:effectiveRange:
- replaceCharactersInRange:withString:
- setAttributes:range:
我們新建一個(gè)NSTextStorage的子類:SyntaxHighlightTextStorage
要實(shí)現(xiàn)以上4個(gè)方法,我們首先需要通過NSMutableAttributedString 實(shí)現(xiàn)一個(gè)后備存儲廷雅,- setAttributes:range:這個(gè)方法需要用beginEditing和endEditing包起來耗美,而且必須調(diào)用 edited:range:changeInLength:京髓,所以大部分的NSTextStorage的子類都長下面這個(gè)樣子:
@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];
}
一個(gè)方便實(shí)現(xiàn)高亮的辦法是覆蓋 -processEditing,并設(shè)置一個(gè)正則表達(dá)式來查找單詞,每次文本存儲有修改時(shí),這個(gè)方法都自動(dòng)被調(diào)用商架。
- (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];
其次遍歷所有的樣式匹配項(xiàng)并高亮它們:
[expression enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
[self addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:result.range];
}];
就這樣堰怨,我們在文本系統(tǒng)棧里面有了一個(gè) Text Storage 的全功能替換版本。在從 Interface 文件中載入時(shí)蛇摸,可以像這樣將它插入文本視圖:
- (void)createTextView {
_textStorage = [SyntaxHighlightTextStorage new];
[_textStorage addLayoutManager: self.textView.layoutManager];
[_textStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:@"在從 Interface 文件中載入時(shí)备图,可以像這樣將它插入文本視圖,然后加 *星號* 的字就會高亮出來了"];
_textView.delegate = self;
}
運(yùn)行如下:
6.文本容器修改:繼承NSTextContainer
通過繼承NSTextContainer,我們可以使得textView不再是一個(gè)規(guī)規(guī)矩矩的矩形赶袄。NSTextContainer負(fù)責(zé)回答這個(gè)問題:對于給定的矩形揽涮,哪個(gè)部分可以放文字,這個(gè)問題由下面這個(gè)方法來回答:
- (CGRect)lineFragmentRectForProposedRect: atIndex: writingDirection: remainingRect:
所以我們在繼承NSTextContainer的類中覆蓋這個(gè)方法即可:
下面這個(gè)方法返回一個(gè)圓形區(qū)域:
- (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);
}
使用這個(gè)繼承類:
- (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的代理方法饿肺,我們可以輕松的設(shè)置行高:
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager
lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex
withProposedLineFragmentRect:(CGRect)rect
{
return floorf(glyphIndex / 100);
}
假設(shè)你的文本中有鏈接蒋困,你不希望這些鏈接被斷行分割。如果可能的話敬辣,一個(gè) URL 應(yīng)該始終顯示為一個(gè)整體雪标,一個(gè)單一的文本片段。沒有什么比這更簡單的了溉跃。
首先村刨,就像前面討論過的那樣,我們使用自定義的 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];
}];
改變斷行行為就只需要實(shí)現(xiàn)一個(gè) 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));
結(jié)果就像下面這樣:
你可以在這里下載完整的代碼烹困。如果你覺得對你有幫助,希望你不吝嗇你的star:)
參考:初識 TextKit,iOS 7 by Tutorials,iOS 7 Programming Pushing the Limits