【iOS】使用CoreText實現圖文混排


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

案例下載地址

https://github.com/ClavisJ/CoreTextDemo

環境信息:

Mac OS X 10.10.1

Xcode 6.1.1

iOS 8.1

 

正文:

一、Core Text簡介

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

Core Text 架構圖

Core Text 架構圖

 

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

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

  • CoreText占用的內容更少,渲染速度更快。UIWebView占用的內存多,渲染速度慢。
  • CoreText在渲染界面的前就可以精確地獲得顯示內容的高度(只要有了CTFrame即可),而WebView只有渲染出內容后,才能獲得內容的高度(而且還需要用JavaScript代碼來獲取)。
  • CoreText的CTFrame可以在后台線程渲染,UIWebView的內容只能在主線程(UI線程)渲染。
  • 基於CoreText可以做更好的原生交互效果,交互效果可以更加細膩。而UIWebView的交互效果都是用JavaScript來實現的,在 交互效果上會有一些卡頓的情況存在。例如,在UIWebView下,一個簡單的按鈕按下的操作,都無法做出原生按鈕的即時和細膩的按下效果。

CoreText排版的劣勢:

  • CoreText渲染出來的內容不能像UIWebView那樣方便地支持內容的復制。
  • 基於CoreText來排版需要自己處理很多復制的邏輯,例如需要自己處理圖片與文字混排相關的邏輯,也需要自己實現連接點擊操作的支持。

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

 

三、繪制純文本

我們創建一個繼承於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繪制純文本

 

四、關於坐標系

上訴代碼的步驟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的作用是改變用戶坐標系統的規模比例。

 

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

可以看到我們使用了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接口,在使用的時候要多加注意引用和釋放 的問題, 更加詳細的解釋可以參照這篇文章

六、圖文混排

終於要開始進行圖文混排了,上面說了那么多,我們來進行一個小結,下圖是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的封裝,感興趣的可以看看。

 

參考資料:

http://geeklu.com/2013/03/core-text/

http://xiangwangfeng.com/2014/03/06/iOS%E6%96%87%E5%AD%97%E6%8E%92%E7%89%88(CoreText)%E9%82%A3%E4%BA%9B%E4%BA%8B/

http://blog.devtang.com/blog/2013/10/21/the-tech-detail-of-ape-client-3/

http://www.yifeiyang.net/development-of-the-iphone-simply-6/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM