1. 程式人生 > >【iOS開發】CoreText的使用(1)

【iOS開發】CoreText的使用(1)

本文技術點涉及到NSAttrbutedString

簡述

CoreText是用於處理文字和字型的底層技術,它直接和Core Graphics(又名Quartz2D)交流。Quartz是一個2D圖形渲染引擎,能夠處理OS X和iOS的圖形顯示問題

Quartz能夠直接處理字型(font)和字形(glyphs)將文字渲染到介面上,它是基礎庫中的唯一能夠處理字形的模組。因此,Core Text為了排版,需要將顯示的文字內容 位置 字型字形直接傳遞給Quartz。與其他UI元件相比由於Core Text直接和Quartz互動 所以它具有搞笑的排版功能。

以下是Core Text架構圖 (iOS7之後)

從這幅圖可以看出 上層UI控制元件包括UILabel UITextField和UIWebView都是基於Core Text實現的。

CoreText和UIWebView的排版比較

CoreText和UIWebView都是處理複雜文字排版的備選方案 對於排版而言 CoreText和UIWebView的比較

優點:

  1. CoreText佔用記憶體小,渲染速度更快 UIWebView佔用記憶體多 渲染速度慢。
  2. CoreText在渲染介面前就可以精確獲得顯示內容高度 (有CTFrame就行)而UIWebView只有在渲染出內容才能獲得內容的高度(通常需要和Javascript程式碼一同使用)
  3. CoreText的CTFrame可以在後臺執行緒渲染,UIWebView的內容只能在主執行緒(UI執行緒)渲染。
  4. 基於CoreText可以做更好的原生互動效果 互動效果更細膩,而UIWebView得互動效果都是用js實現,在互動效果下會有卡頓情況的存在,例如 在UIWebView下一個簡單的按鈕點選事件 和CoreText就沒辦法比
劣勢:
CoreText渲染出來的內容不能像UIwebView那樣方便複製
基於CoreText排版需要自己處理很多複雜的邏輯 例如需要自己處理圖片與文字混排相關邏輯,也需要自己實現連線點選操作支援。

CoreText使用情景包括:新浪微部落格戶端,多看閱讀客戶端,猿題庫。

Core Text內部結構如圖


上圖中大概顯示了後半部分的結構,CTFrame 是指整個該UIView子控制元件的繪製區域,CTLine則是指每一行。CTRun則是每一段具有一樣的屬性的字串 比如某段字型大小,顏色都一致的字串為一個CTRun。CTRun不可以跨行,不管屬性是否一致。通常的結構是每一個CTFrame有多個CTLine 每一個CTLine有多個CTRun。

一個簡單的不包含圖片連結等的文字顯示demo的過程如下

  1. 獲取上下文
  2. 翻轉座標系
  3. 建立NSAttributedString
  4. 根據NSAttributedString建立CTFrameSetterRef物件
  5. 建立繪製區域CGPathRef
  6. 根據CTFramesetterRef和CGPathRef建立CTFrame
  7. CTFrameDraw繪製
  8. 手動釋放建立的CTFramesetter CTFrame CGPathRef物件。

子控制元件的drawInRect程式碼如下:

- (void)drawRect:(CGRect)rect
{
    
    [super drawRect:rect];
    
    // 1.獲取上下文
    CGContextRef contextRef = UIGraphicsGetCurrentContext();
    
    // [a,b,c,d,tx,ty]
    NSLog(@"轉換前的座標:%@",NSStringFromCGAffineTransform(CGContextGetCTM(contextRef)));
    
    // 2.轉換座標系,CoreText的原點在左下角,UIKit原點在左上角
    CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
    
    // 這兩種轉換座標的方式效果一樣
    // 2.1
    // CGContextTranslateCTM(contextRef, 0, self.bounds.size.height);
    // CGContextScaleCTM(contextRef, 1.0, -1.0);
    
    // 2.2
    CGContextConcatCTM(contextRef, CGAffineTransformMake(1, 0, 0, -1, 0, self.bounds.size.height));
    
    NSLog(@"轉換後的座標:%@",NSStringFromCGAffineTransform(CGContextGetCTM(contextRef)));
    
    
    // 3.建立繪製區域,可以對path進行個性化裁剪以改變顯示區域
    CGMutablePathRef path = CGPathCreateMutable();
    //CGPathAddRect(path, NULL, self.bounds);
     CGPathAddEllipseInRect(path, NULL, self.bounds);
    
    // 4.建立需要繪製的文字
    NSMutableAttributedString *attributed = [[NSMutableAttributedString alloc] initWithString:@"關關雎鳩,在河之洲。窈窕淑女,君子好逑。 參差荇菜,左右流之。窈窕淑女,寤寐求之。求之不得,寤寐思服。悠哉悠哉,輾轉反側。參差荇菜,左右采之。窈窕淑女,琴瑟友之。參差荇菜,左右芼之。窈窕淑女,鐘鼓樂之。"];
    
    [attributed addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:18] range:NSMakeRange(0, 6)];
    
    // 兩種方式皆可
    [attributed addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(3, 10)];
    [attributed addAttribute:(id)kCTForegroundColorAttributeName value:[UIColor greenColor] range:NSMakeRange(0, 2)];
    
    // 設定行距等樣式
    CGFloat lineSpace = 10; // 行距一般取決於這個值
    CGFloat lineSpaceMax = 20;
    CGFloat lineSpaceMin = 2;
    const CFIndex kNumberOfSettings = 3;
    
    // 結構體陣列
    CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
        
        {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpace},
        {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpaceMax},
        {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpaceMin}
    };
    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
    
    // 單個元素的形式
    // CTParagraphStyleSetting theSettings = {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpace};
    // CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(&theSettings, kNumberOfSettings);
    
    // 兩種方式皆可
    // [attributed addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0, attributed.length)];
    
    // 將設定的行距應用於整段文字
    [attributed addAttribute:NSParagraphStyleAttributeName value:(__bridge id)(theParagraphRef) range:NSMakeRange(0, attributed.length)];
    
    CFRelease(theParagraphRef);
    
    // 5.根據NSAttributedString生成CTFramesetterRef
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributed);
    
    CTFrameRef ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, NULL);
    
    // 6.繪製除圖片以外的部分
    CTFrameDraw(ctFrame, contextRef);
    // 7.記憶體管理,ARC不能管理CF開頭的物件,需要我們自己手動釋放記憶體
    CFRelease(path);
    CFRelease(framesetter);
    CFRelease(ctFrame);
}
執行效果如下:

程式碼注意點:

  1. DrawInRect方法不是本文重點 不解釋
  2. 為什麼翻轉?座標系原點是在左下角,UIKit的座標原點在左上角。
  3. 建立繪製區域 

    CGMutablePathRef path = CGPathCreateMutable();

    CGPathAddRect(path, NULL, self.bounds);

    當前是用Quartz建立了矩形加入到區域路徑中,如果替換為

    CGPathAddEllipseInRect(path,NULL,self.bounds),則會發現可繪製區域變成一個橢圓。

下一篇將會涉及到圖文混排的內容。