Text Kit進階

Text Rendering Architecture

在上一篇文章Text Kit入門中咱們主要了解了什麼是Text Kit及它的一些架構和基本特性,這篇文章中會涉及關於Text Kit的更多具體應用。html

Text Kit是創建在Core Text框架上的,咱們知道CoreText.framework是一個龐大而複雜的框架,而Text Kit在繼承了Core Text強大功能的同時給開發者提供了比較友好的面向對象的API。ios

本文主要介紹Text Kit下面四個特性:git

  • 動態字體(Dynamic type)
  • 凸版印刷體效果(Letterpress effects)
  • 路徑排除(Exclusion paths)
  • 動態文本格式化和存儲(Dynamic text formatting and storage)

Dynamic type

動態字體是iOS7中新增長的比較重要的特性之一,程序應該按照用戶設定的字體大小和粗細來顯示文本內容。github

分別在設置\通用\輔助功能設置\通用\文字大小中能夠設置文本在應用程序中顯示的粗細和大小。正則表達式

Font Weight Font Size

iOS7對系統字體在顯示上作了一些優化,讓不一樣大小的字體在屏幕上都能清晰的顯示。一般用戶設置了本身偏好的字體,他們但願在全部程序中都看到文本顯示是根據他們的設定進行調整。爲了實現這個,開發者須要在本身的應用中給文本控件設置當前用戶設置字體,而不是指定死字體及大小。能夠經過UIFont中新增的preferredFontForTextStyle:方法來獲取用戶偏好的字體。設計模式

iOS7中給出了6中字體樣式供選擇:架構

  • UIFontTextStyleHeadline
  • UIFontTextStyleBody
  • UIFontTextStyleSubheadline
  • UIFontTextStyleFootnote
  • UIFontTextStyleCaption1
  • UIFontTextStyleCaption2

Text Style

爲了讓咱們的程序支持動態字體,須要按一下方式給文本控件(一般是指UILabelUITextFieldUITextView)設定字體:app

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];

Letterpress

在iOS7系統自帶的備忘錄應用中,蘋果就使用了這種凸版印刷體效果。

Exclusion paths

在排版中,圖文混排是很是常見的需求,但有時候咱們的圖片並必定都是正常的矩形,這個時候咱們若是須要將文本環繞在圖片周圍,就能夠用路徑排除(exclusion paths)了。

Explosion pats基本原理是將須要被文本留出來的形狀的路徑告訴文本控件的NSTextContainer對象,NSTextContainer在文字排版時就會避開該路徑。

UIBezierPath *floatingPath = [self pathOfImage];
self.textView.textContainer.exclusionPaths = @[floatingPath];

Exclusion paths

因此實現Exclusion paths的主要工做就是獲取這個path。

Dynamic text formatting and storage

好了,到如今咱們知道了Text Kit能夠動態的根據用戶設置的字體大小進行調整,可是若是具體某個文本顯示控件中的文本樣式可以動態調整是否是會更酷一些呢?

例如,你但願讓你的textView中的文本自動支持下面功能:

  • **字符之間的文本加粗顯示
  • _字符之間的文本以斜體字顯示
  • ~~字符之間的文本以被橫線穿透樣式顯示
  • 讓全大寫的文本以紅色字體顯示

Text Formatting Demo

實現這些纔是真正體現Text Kit強大之處的時候,在此以前你須要理解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

相關文章
相關標籤/搜索