使用CoreText實現圖文混排

OS沒有現成的支持圖文混排的控件,而要用多個基礎控件組合拼成圖文混排這樣複雜的排版,是件很苦逼的事情。對此的解決方案有使用CoreText進行繪製,或者使用TextKit。本文主要講解對於CoreText的使用。git

案例下載地址

https://github.com/ClavisJ/CoreTextDemogithub

環境信息:

Mac OS X 10.10.1數組

Xcode 6.1.1網絡

iOS 8.1多線程

正文:

1、Core Text簡介

CoreText是基於IOS3.2及OSX10.5的用於文字精細排版的文本框架。它直接與Core Graphics(又稱:Quartz)交互,將須要顯示的文本內容,位置,字體,字形直接傳遞給Quartz,與其餘UI組件相比,能更高效的進行渲染。架構

Core Text 架構圖

Core Text 架構圖框架

 

2、CoreText與UIWebView在排版方面的優劣比較

UIWebView也經常使用於處理複雜的排版,對應排版他們之間的優劣以下(摘自 《iOS開發進階》—— 唐巧):iphone

  • CoreText佔用的內容更少,渲染速度更快。UIWebView佔用的內存多,渲染速度慢。函數

  • CoreText在渲染界面的前就能夠精確地得到顯示內容的高度(只要有了CTFrame便可),而WebView只有渲染出內容後,才能得到內容的高度(並且還須要用JavaScript代碼來獲取)。字體

  • CoreText的CTFrame能夠在後臺線程渲染,UIWebView的內容只能在主線程(UI線程)渲染。

  • 基於CoreText能夠作更好的原生交互效果,交互效果能夠更加細膩。而UIWebView的交互效果都是用JavaScript來實現的,在交互效果上會有一些卡頓的狀況存在。例如,在UIWebView下,一個簡單的按鈕按下的操做,都沒法作出原生按鈕的即時和細膩的按下效果。

CoreText排版的劣勢:

  • CoreText渲染出來的內容不能像UIWebView那樣方便地支持內容的複製。

  • 基於CoreText來排版須要本身處理不少複製的邏輯,例如須要本身處理圖片與文字混排相關的邏輯,也須要本身實現鏈接點擊操做的支持。

在業界有不少應用都採用CoreText技術進行排版,例如新浪微博客戶端,多看閱讀客戶端,猿題庫等等。

 

3、繪製純文本

咱們建立一個繼承於UIView的類,重寫他的drawRect方法,來繪製純文本。

- (void)drawRect:(CGRect)rect {
    
    [super drawRect:rect];    
    // 步驟1:獲得當前用於繪製畫布的上下文,用於後續將內容繪製在畫布上
    // 由於Core Text要配合Core Graphic 配合使用的,如Core Graphic同樣,繪圖的時候須要得到當前的上下文進行繪製
    CGContextRef context = UIGraphicsGetCurrentContext();    
    // 步驟2:翻轉當前的座標系(由於對於底層繪製引擎來講,屏幕左下角爲(0,0))
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);    CGContextTranslateCTM(context, 0, self.bounds.size.height);    CGContextScaleCTM(context, 1.0, -1.0);    
    // 步驟3:建立繪製區域
    CGMutablePathRef path = CGPathCreateMutable();    CGPathAddEllipseInRect(path, NULL, self.bounds);    
    // 步驟4:建立須要繪製的文字與計算須要繪製的區域
    NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:@"iOS程序在啓動時會建立一個主線程,而在一個線程只能執行一件事情,若是在主線程執行某些耗時操做,例如加載網絡圖片,下載資源文件等會阻塞主線程(致使界面卡死,沒法交互),因此就須要使用多線程技術來避免這類狀況。iOS中有三種多線程技術 NSThread,NSOperation,GCD,這三種技術是隨着IOS發展引入的,抽象層次由低到高,使用也愈來愈簡單。"];    // 步驟5:根據AttributedString生成CTFramesetterRef
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, [attrString length]), path, NULL);    // 步驟6:進行繪製
    CTFrameDraw(frame, context);    // 步驟7.內存管理
    CFRelease(frame);    CFRelease(path);    CFRelease(frameSetter);
}

運行的效果以下圖

CoreText繪製純文本

CoreText繪製純文本

 

4、關於座標系

上訴代碼的步驟2對繪圖的座標系進行了處理,由於在iOS UIKit中,UIView是以左上角爲原點,而Core Text一開始的定位是使用與桌面應用的排版系統,桌面應用的座標系是以左下角爲原點,即Core Text在繪製的時候也是參照左下角爲原點進行繪製的,因此須要對當前的座標系進行處理。

實際上,Core Graphic 中的context也是以左下角爲原點的, 可是爲何咱們用Core Graphic 繪製一些簡單的圖形的時候不須要對座標系進行處理喃,是由於經過這個方法UIGraphicsGetCurrentContext()來得到的當前context是已經被處理過的了,用下面方法能夠查看指定的上下文的當前圖形狀態變換矩陣。

NSLog(@"當前context的變換矩陣 %@", NSStringFromCGAffineTransform(CGContextGetCTM(context)));

打印結果爲[2, 0, 0, -2, 0, 654],能夠發現變換矩陣與CGAffineTransformIdentity的值[1, 0, 0, 1, 0, 0]是不相同的,而且與設備是否爲Retina屏和設備尺寸相關。他的做用是將上下文空間座標系進行翻轉,並使原來的左下角原點變成右上角是原點,並將向上爲正y軸變爲向下爲正y軸。 因此在使用drawRect的時候,當前的context已經被作了一次翻轉,若是不對當前的座標系進行處理,會發現,繪製出來的文字是鏡像上下顛倒的,如圖

不處理context

不處理context

因此須要先重置當前的座標系翻轉狀態,在進行一次翻轉,處理以後的矩陣爲[2, 0, -0, 2, 0, 0],函數CGContextTranslateCTM的做用變換座標系中的原點,函數CGContextScaleCTM的做用是改變用戶座標系統的規模比例。

 

5、自定義文本的顏色,字體與行間距

能夠看到咱們使用了NSMutableAttributedString這個類來描述須要繪製的文字,而一個NSMutableAttributedString對象能夠包含不少屬性,每個屬性都有起對應的字符區域,咱們能夠用這些屬性來描述文本中特殊的顏色和字體。

- (void)drawRect:(CGRect)rect {    // 省略前面的步驟1-4

    // 步驟8:設置部分文字顏色
    [attrString addAttribute:(id)kCTForegroundColorAttributeName value:[UIColor greenColor] range:NSMakeRange(10, 10)];    
    // 設置部分文字
    CGFloat fontSize = 20;
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
    [attrString addAttribute:(id)kCTFontAttributeName value:(__bridge id)fontRef range:NSMakeRange(15, 10)];    CFRelease(fontRef);   
    // 設置行間距
    CGFloat lineSpacing = 10;    const CFIndex kNumberOfSettings = 3;
    CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
        {kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing},
        {kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing},
        {kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing}
    };
    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
    [attrString addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0, attrString.length)];    CFRelease(theParagraphRef);    // 省略以後的步驟5-7}

最終的效果以下

自定義文本屬性

自定義文本屬性

提示:在配置NSMutableAttributedString 的Attribute的時候,用到了不少這樣的(__bridge id)標識,來解釋下:這個由於addAttribute:是OC的方法,須要Object C 對象,而CTParagraphStyleRef這些是由C語言實現的Core Foundation Framework 框架中的對象,這兩種類型能夠相互轉換和操做。Core Foundation Framework 框架中的對象也有引用計數的概念,可是不是Cocoa Framework中的release/retain不一樣,而是使用自身的CFRetain/CFRelease接口,在使用的時候要多加註意引用和釋放的問題, 更加詳細的解釋能夠參照這篇文章

6、圖文混排

終於要開始進行圖文混排了,上面說了那麼多,咱們來進行一個小結,下圖是CoreText繪製的流程圖與CTFrame和CTLine,CTRun之間的關係:

CoreText繪製的流程圖,CTFrame和CTLine CTRun之間的關係

CoreText繪製的流程圖,CTFrame和CTLine CTRun之間的關係

 

咱們來解釋一下這些類:

CFAttributedStringRef :屬性字符串,用於存儲須要繪製的文字字符和字符屬性

CTFramesetterRef:經過CFAttributedStringRef進行初始化,做爲CTFrame對象的生產工廠,負責根據path建立對應的CTFrame

CTFrame:用於繪製文字的類,能夠經過CTFrameDraw函數,直接將文字繪製到context上

CTLine:在CTFrame內部是由多個CTLine來組成的,每一個CTLine表明一行

CTRun:每一個CTLine又是由多個CTRun組成的,每一個CTRun表明一組顯示風格一致的文本

實際上CoreText是不直接支持繪製圖片的,可是咱們能夠先在須要顯示圖片的地方用一個特殊的空白佔位符代替,同時設置該字體的CTRunDelegate信息爲要顯示的圖片的寬度和高度,這樣繪製文字的時候就會先把圖片的位置留出來,再在drawRect方法裏面用CGContextDrawImage繪製圖片。

- (void)drawRect:(CGRect)rect {
    
    [super drawRect:rect];    // 省略步驟1-4  ,步驟8
    // 步驟9:圖文混排部分
    // CTRunDelegateCallbacks:一個用於保存指針的結構體,由CTRun delegate進行回調
    CTRunDelegateCallbacks callbacks;
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getDescent = descentCallback;
    callbacks.getWidth = widthCallback;    
    // 圖片信息字典
    NSDictionary *imgInfoDic = @{@"width":@100,@"height":@30};    
    // 設置CTRun的代理
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)imgInfoDic);    
    // 使用0xFFFC做爲空白的佔位符
    unichar objectReplacementChar = 0xFFFC;    NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];    NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content];    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);    CFRelease(delegate); 
    // 將建立的空白AttributedString插入進當前的attrString中,位置能夠隨便指定,不能越界
    [attrString insertAttributedString:space atIndex:50];    
    // 省略步驟5-6
    
    // 步驟10:繪製圖片
    UIImage *image = [UIImage imageNamed:@"coretext-img-1.png"];    CGContextDrawImage(context, [self calculateImagePositionInCTFrame:frame], image.CGImage);   
    // 省略步驟7
 } 
 #pragma mark - CTRun delegate 回調方法

 static CGFloat ascentCallback(void *ref) {    
    return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
 } static CGFloat descentCallback(void *ref) {    
    return 0;
 } 
 static CGFloat widthCallback(void *ref) {    
    return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
 } 
 /**
 *  根據CTFrameRef得到繪製圖片的區域
 *
 *  @param ctFrame CTFrameRef對象
 *
 *  @return繪製圖片的區域
 */
 - (CGRect)calculateImagePositionInCTFrame:(CTFrameRef)ctFrame {    
    // 得到CTLine數組
    NSArray *lines = (NSArray *)CTFrameGetLines(ctFrame);    NSInteger lineCount = [lines count];    CGPoint lineOrigins[lineCount];
    CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins);    
    // 遍歷每一個CTLine
    for (NSInteger i = 0 ; i < lineCount; i++) {
        
        CTLineRef line = (__bridge CTLineRef)lines[i];        NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);        
        // 遍歷每一個CTLine中的CTRun
        for (id runObj in runObjArray) {
            
            CTRunRef run = (__bridge CTRunRef)runObj;            NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];            if (delegate == nil) {                continue;
            }            
            NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);            if (![metaDic isKindOfClass:[NSDictionary class]]) {                continue;
            }            
            CGRect runBounds;            CGFloat ascent;            CGFloat descent;
            
            runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            runBounds.size.height = ascent + descent;            
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            runBounds.origin.x = lineOrigins[i].x + xOffset;
            runBounds.origin.y = lineOrigins[i].y;
            runBounds.origin.y -= descent;            
            CGPathRef pathRef = CTFrameGetPath(ctFrame);            CGRect colRect = CGPathGetBoundingBox(pathRef);            
            CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);            return delegateBounds;
        }
    }    return CGRectZero;
 }

 至此咱們就完成了使用CoreText進行圖文混排,上面得到圖片位置的方法只能得到第一張圖片位置,你們能夠自行完善一下,用數組來進行存儲圖片繪製區域。唐巧在《iOS開發進階》一書中更多的介紹了對CoreText的封裝,感興趣的能夠看看。

相關文章
相關標籤/搜索