使用CoreText实现图文混排
2019獨角獸企業重金招聘Python工程師標準>>>
OS沒有現成的支持圖文混排的控件,而要用多個基礎控件組合拼成圖文混排這樣復雜的排版,是件很苦逼的事情。對此的解決方案有使用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 架構圖
?
二、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生成CTFramesetterRefCTFramesetterRef?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繪制純文本
?
四、關于坐標系
上訴代碼的步驟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
所以需要先重置當前的坐標系翻轉狀態,在進行一次翻轉,處理之后的矩陣為[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之間的關系
?
我們來解釋一下這些類:
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);????//?遍歷每個CTLinefor?(NSInteger?i?=?0?;?i?<?lineCount;?i++)?{CTLineRef?line?=?(__bridge?CTLineRef)lines[i];????????NSArray?*runObjArray?=?(NSArray?*)CTLineGetGlyphRuns(line);????????//?遍歷每個CTLine中的CTRunfor?(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的封裝,感興趣的可以看看。
轉載于:https://my.oschina.net/u/2361492/blog/526814
總結
以上是生活随笔為你收集整理的使用CoreText实现图文混排的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jQuery学习笔记之DOM操作、事件绑
- 下一篇: JsonMappingException