使用CoreText实现图文混排
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
OS沒(méi)有現(xiàn)成的支持圖文混排的控件,而要用多個(gè)基礎(chǔ)控件組合拼成圖文混排這樣復(fù)雜的排版,是件很苦逼的事情。對(duì)此的解決方案有使用CoreText進(jìn)行繪制,或者使用TextKit。本文主要講解對(duì)于CoreText的使用。
案例下載地址
https://github.com/ClavisJ/CoreTextDemo
環(huán)境信息:
Mac OS X 10.10.1
Xcode 6.1.1
iOS 8.1
正文:
一、Core Text簡(jiǎn)介
CoreText是基于IOS3.2及OSX10.5的用于文字精細(xì)排版的文本框架。它直接與Core Graphics(又稱(chēng):Quartz)交互,將需要顯示的文本內(nèi)容,位置,字體,字形直接傳遞給Quartz,與其他UI組件相比,能更高效的進(jìn)行渲染。
Core Text 架構(gòu)圖
?
二、CoreText與UIWebView在排版方面的優(yōu)劣比較
UIWebView也常用于處理復(fù)雜的排版,對(duì)應(yīng)排版他們之間的優(yōu)劣如下(摘自 《iOS開(kāi)發(fā)進(jìn)階》—— 唐巧):
CoreText占用的內(nèi)容更少,渲染速度更快。UIWebView占用的內(nèi)存多,渲染速度慢。
CoreText在渲染界面的前就可以精確地獲得顯示內(nèi)容的高度(只要有了CTFrame即可),而WebView只有渲染出內(nèi)容后,才能獲得內(nèi)容的高度(而且還需要用JavaScript代碼來(lái)獲取)。
CoreText的CTFrame可以在后臺(tái)線程渲染,UIWebView的內(nèi)容只能在主線程(UI線程)渲染。
基于CoreText可以做更好的原生交互效果,交互效果可以更加細(xì)膩。而UIWebView的交互效果都是用JavaScript來(lái)實(shí)現(xiàn)的,在交互效果上會(huì)有一些卡頓的情況存在。例如,在UIWebView下,一個(gè)簡(jiǎn)單的按鈕按下的操作,都無(wú)法做出原生按鈕的即時(shí)和細(xì)膩的按下效果。
CoreText排版的劣勢(shì):
CoreText渲染出來(lái)的內(nèi)容不能像UIWebView那樣方便地支持內(nèi)容的復(fù)制。
基于CoreText來(lái)排版需要自己處理很多復(fù)制的邏輯,例如需要自己處理圖片與文字混排相關(guān)的邏輯,也需要自己實(shí)現(xiàn)連接點(diǎn)擊操作的支持。
在業(yè)界有很多應(yīng)用都采用CoreText技術(shù)進(jìn)行排版,例如新浪微博客戶端,多看閱讀客戶端,猿題庫(kù)等等。
?
三、繪制純文本
我們創(chuàng)建一個(gè)繼承于UIView的類(lèi),重寫(xiě)他的drawRect方法,來(lái)繪制純文本。
-?(void)drawRect:(CGRect)rect?{[super?drawRect:rect];????//?步驟1:得到當(dāng)前用于繪制畫(huà)布的上下文,用于后續(xù)將內(nèi)容繪制在畫(huà)布上//?因?yàn)镃ore?Text要配合Core?Graphic?配合使用的,如Core?Graphic一樣,繪圖的時(shí)候需要獲得當(dāng)前的上下文進(jìn)行繪制CGContextRef?context?=?UIGraphicsGetCurrentContext();????//?步驟2:翻轉(zhuǎn)當(dāng)前的坐標(biāo)系(因?yàn)閷?duì)于底層繪制引擎來(lái)說(shuō),屏幕左下角為(0,0))CGContextSetTextMatrix(context,?CGAffineTransformIdentity);????CGContextTranslateCTM(context,?0,?self.bounds.size.height);????CGContextScaleCTM(context,?1.0,?-1.0);????//?步驟3:創(chuàng)建繪制區(qū)域CGMutablePathRef?path?=?CGPathCreateMutable();????CGPathAddEllipseInRect(path,?NULL,?self.bounds);????//?步驟4:創(chuàng)建需要繪制的文字與計(jì)算需要繪制的區(qū)域NSMutableAttributedString?*attrString?=?[[NSMutableAttributedString?alloc]?initWithString:@"iOS程序在啟動(dòng)時(shí)會(huì)創(chuàng)建一個(gè)主線程,而在一個(gè)線程只能執(zhí)行一件事情,如果在主線程執(zhí)行某些耗時(shí)操作,例如加載網(wǎng)絡(luò)圖片,下載資源文件等會(huì)阻塞主線程(導(dǎo)致界面卡死,無(wú)法交互),所以就需要使用多線程技術(shù)來(lái)避免這類(lèi)情況。iOS中有三種多線程技術(shù)?NSThread,NSOperation,GCD,這三種技術(shù)是隨著IOS發(fā)展引入的,抽象層次由低到高,使用也越來(lái)越簡(jiǎn)單。"];????//?步驟5:根據(jù)AttributedString生成CTFramesetterRefCTFramesetterRef?frameSetter?=?CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);CTFrameRef?frame?=?CTFramesetterCreateFrame(frameSetter,?CFRangeMake(0,?[attrString?length]),?path,?NULL);????//?步驟6:進(jìn)行繪制CTFrameDraw(frame,?context);????//?步驟7.內(nèi)存管理CFRelease(frame);????CFRelease(path);????CFRelease(frameSetter); }運(yùn)行的效果如下圖
CoreText繪制純文本
?
四、關(guān)于坐標(biāo)系
上訴代碼的步驟2對(duì)繪圖的坐標(biāo)系進(jìn)行了處理,因?yàn)樵趇OS UIKit中,UIView是以左上角為原點(diǎn),而Core Text一開(kāi)始的定位是使用與桌面應(yīng)用的排版系統(tǒng),桌面應(yīng)用的坐標(biāo)系是以左下角為原點(diǎn),即Core Text在繪制的時(shí)候也是參照左下角為原點(diǎn)進(jìn)行繪制的,所以需要對(duì)當(dāng)前的坐標(biāo)系進(jìn)行處理。
實(shí)際上,Core Graphic 中的context也是以左下角為原點(diǎn)的, 但是為什么我們用Core Graphic 繪制一些簡(jiǎn)單的圖形的時(shí)候不需要對(duì)坐標(biāo)系進(jìn)行處理喃,是因?yàn)橥ㄟ^(guò)這個(gè)方法UIGraphicsGetCurrentContext()來(lái)獲得的當(dāng)前context是已經(jīng)被處理過(guò)的了,用下面方法可以查看指定的上下文的當(dāng)前圖形狀態(tài)變換矩陣。
NSLog(@"當(dāng)前context的變換矩陣?%@",?NSStringFromCGAffineTransform(CGContextGetCTM(context)));打印結(jié)果為[2, 0, 0, -2, 0, 654],可以發(fā)現(xiàn)變換矩陣與CGAffineTransformIdentity的值[1, 0, 0, 1, 0, 0]是不相同的,并且與設(shè)備是否為Retina屏和設(shè)備尺寸相關(guān)。他的作用是將上下文空間坐標(biāo)系進(jìn)行翻轉(zhuǎn),并使原來(lái)的左下角原點(diǎn)變成右上角是原點(diǎn),并將向上為正y軸變?yōu)橄蛳聻檎齳軸。 所以在使用drawRect的時(shí)候,當(dāng)前的context已經(jīng)被做了一次翻轉(zhuǎn),如果不對(duì)當(dāng)前的坐標(biāo)系進(jìn)行處理,會(huì)發(fā)現(xiàn),繪制出來(lái)的文字是鏡像上下顛倒的,如圖
不處理context
所以需要先重置當(dāng)前的坐標(biāo)系翻轉(zhuǎn)狀態(tài),在進(jìn)行一次翻轉(zhuǎn),處理之后的矩陣為[2, 0, -0, 2, 0, 0],函數(shù)CGContextTranslateCTM的作用變換坐標(biāo)系中的原點(diǎn),函數(shù)CGContextScaleCTM的作用是改變用戶坐標(biāo)系統(tǒng)的規(guī)模比例。
?
五、自定義文本的顏色,字體與行間距
可以看到我們使用了NSMutableAttributedString這個(gè)類(lèi)來(lái)描述需要繪制的文字,而一個(gè)NSMutableAttributedString對(duì)象可以包含很多屬性,每一個(gè)屬性都有起對(duì)應(yīng)的字符區(qū)域,我們可以用這些屬性來(lái)描述文本中特殊的顏色和字體。
-?(void)drawRect:(CGRect)rect?{????//?省略前面的步驟1-4//?步驟8:設(shè)置部分文字顏色[attrString?addAttribute:(id)kCTForegroundColorAttributeName?value:[UIColor?greenColor]?range:NSMakeRange(10,?10)];????//?設(shè)置部分文字CGFloat?fontSize?=?20;CTFontRef?fontRef?=?CTFontCreateWithName((CFStringRef)@"ArialMT",?fontSize,?NULL);[attrString?addAttribute:(id)kCTFontAttributeName?value:(__bridge?id)fontRef?range:NSMakeRange(15,?10)];????CFRelease(fontRef);???//?設(shè)置行間距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的時(shí)候,用到了很多這樣的(__bridge?id)標(biāo)識(shí),來(lái)解釋下:這個(gè)因?yàn)?span style="border:0px;font-family:inherit;font-style:inherit;font-weight:inherit;vertical-align:baseline;color:rgb(61,29,129);">addAttribute:是OC的方法,需要Object C 對(duì)象,而CTParagraphStyleRef這些是由C語(yǔ)言實(shí)現(xiàn)的Core Foundation Framework 框架中的對(duì)象,這兩種類(lèi)型可以相互轉(zhuǎn)換和操作。Core Foundation Framework 框架中的對(duì)象也有引用計(jì)數(shù)的概念,但是不是Cocoa Framework中的release/retain不同,而是使用自身的CFRetain/CFRelease接口,在使用的時(shí)候要多加注意引用和釋放的問(wèn)題, 更加詳細(xì)的解釋可以參照這篇文章。
六、圖文混排
終于要開(kāi)始進(jìn)行圖文混排了,上面說(shuō)了那么多,我們來(lái)進(jìn)行一個(gè)小結(jié),下圖是CoreText繪制的流程圖與CTFrame和CTLine,CTRun之間的關(guān)系:
CoreText繪制的流程圖,CTFrame和CTLine CTRun之間的關(guān)系
?
我們來(lái)解釋一下這些類(lèi):
CFAttributedStringRef :屬性字符串,用于存儲(chǔ)需要繪制的文字字符和字符屬性
CTFramesetterRef:通過(guò)CFAttributedStringRef進(jìn)行初始化,作為CTFrame對(duì)象的生產(chǎn)工廠,負(fù)責(zé)根據(jù)path創(chuàng)建對(duì)應(yīng)的CTFrame
CTFrame:用于繪制文字的類(lèi),可以通過(guò)CTFrameDraw函數(shù),直接將文字繪制到context上
CTLine:在CTFrame內(nèi)部是由多個(gè)CTLine來(lái)組成的,每個(gè)CTLine代表一行
CTRun:每個(gè)CTLine又是由多個(gè)CTRun組成的,每個(gè)CTRun代表一組顯示風(fēng)格一致的文本
實(shí)際上CoreText是不直接支持繪制圖片的,但是我們可以先在需要顯示圖片的地方用一個(gè)特殊的空白占位符代替,同時(shí)設(shè)置該字體的CTRunDelegate信息為要顯示的圖片的寬度和高度,這樣繪制文字的時(shí)候就會(huì)先把圖片的位置留出來(lái),再在drawRect方法里面用CGContextDrawImage繪制圖片。
-?(void)drawRect:(CGRect)rect?{[super?drawRect:rect];????//?省略步驟1-4??,步驟8//?步驟9:圖文混排部分//?CTRunDelegateCallbacks:一個(gè)用于保存指針的結(jié)構(gòu)體,由CTRun?delegate進(jìn)行回調(diào)CTRunDelegateCallbacks?callbacks;memset(&callbacks,?0,?sizeof(CTRunDelegateCallbacks));callbacks.version?=?kCTRunDelegateVersion1;callbacks.getAscent?=?ascentCallback;callbacks.getDescent?=?descentCallback;callbacks.getWidth?=?widthCallback;????//?圖片信息字典N(xiāo)SDictionary?*imgInfoDic?=?@{@"width":@100,@"height":@30};????//?設(shè)置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);?//?將創(chuàng)建的空白AttributedString插入進(jìn)當(dāng)前的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?回調(diào)方法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];}?/***??根據(jù)CTFrameRef獲得繪制圖片的區(qū)域**??@param?ctFrame?CTFrameRef對(duì)象**??@return繪制圖片的區(qū)域*/-?(CGRect)calculateImagePositionInCTFrame:(CTFrameRef)ctFrame?{????//?獲得CTLine數(shù)組NSArray?*lines?=?(NSArray?*)CTFrameGetLines(ctFrame);????NSInteger?lineCount?=?[lines?count];????CGPoint?lineOrigins[lineCount];CTFrameGetLineOrigins(ctFrame,?CFRangeMake(0,?0),?lineOrigins);????//?遍歷每個(gè)CTLinefor?(NSInteger?i?=?0?;?i?<?lineCount;?i++)?{CTLineRef?line?=?(__bridge?CTLineRef)lines[i];????????NSArray?*runObjArray?=?(NSArray?*)CTLineGetGlyphRuns(line);????????//?遍歷每個(gè)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進(jìn)行圖文混排,上面獲得圖片位置的方法只能獲得第一張圖片位置,大家可以自行完善一下,用數(shù)組來(lái)進(jìn)行存儲(chǔ)圖片繪制區(qū)域。唐巧在《iOS開(kāi)發(fā)進(jìn)階》一書(shū)中更多的介紹了對(duì)CoreText的封裝,感興趣的可以看看。
轉(zhuǎn)載于:https://my.oschina.net/u/2361492/blog/526814
總結(jié)
以上是生活随笔為你收集整理的使用CoreText实现图文混排的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: jQuery学习笔记之DOM操作、事件绑
- 下一篇: JsonMappingException