CoreText圖文混排

最近在網(wǎng)上看了一些大牛的文章,自己也試著寫(xiě)了一下,感覺(jué)圖文混排真的很強(qiáng)大。

廢話(huà)不多說(shuō),開(kāi)始整

先上效果圖跟代碼,然后一步步一句句給你分析


這個(gè)圖是我的APP上面做的一個(gè)示范,后面內(nèi)容不重要,圖文混排的部分是紅色背景區(qū)域

代碼

#import "coretext.h"#import@implementation coretext

-(void)drawRect:(CGRect)rect

{

[super drawRect:rect];

CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSetTextMatrix(context, CGAffineTransformIdentity);? ? CGContextTranslateCTM(context, 0, self.bounds.size.height);

CGContextScaleCTM(context, 1.0, -1.0);

NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\n這里在測(cè)試圖文混排,\n我是一個(gè)富文本"];

CTRunDelegateCallbacks callBacks;

memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));

callBacks.version = kCTRunDelegateVersion1;

callBacks.getAscent = ascentCallBacks;

callBacks.getDescent = descentCallBacks;

callBacks.getWidth = widthCallBacks;

NSDictionary * dicPic = @{@"height":@50,@"width":@50};

CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);

unichar placeHolder = 0xFFFC;

NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];

NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];

CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);

CFRelease(delegate);

[attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];

CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);

CGMutablePathRef path = CGPathCreateMutable();

CGPathAddRect(path, NULL, self.bounds);

NSInteger length = attributeStr.length;

CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);

CTFrameDraw(frame, context);

UIImage * image = [UIImage imageNamed:@"默認(rèn)頭像"];

CGRect imgFrm = [self calculateImageRectWithFrame:frame];

CGContextDrawImage(context,imgFrm, image.CGImage);

CFRelease(frame);

CFRelease(path);

CFRelease(frameSetter);

}

static CGFloat ascentCallBacks(void * ref)

{

return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue];

}

static CGFloat descentCallBacks(void * ref)

{

return 0;

}

static CGFloat widthCallBacks(void * ref)

{

return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue];

}

-(CGRect)calculateImageRectWithFrame:(CTFrameRef)frame

{

NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);

NSInteger count = [arrLines count];

CGPoint points[count];

CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);

for (int i = 0; i < count; i ++) {

CTLineRef line = (__bridge CTLineRef)arrLines[i];

NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);

for (int j = 0; j < arrGlyphRun.count; j ++) {

CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];

NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);? ? ? ? ? ? CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];

if (delegate == nil) {

continue;

}

NSDictionary * dic = CTRunDelegateGetRefCon(delegate);

if (![dic isKindOfClass:[NSDictionary class]]) {

continue;

}

CGPoint point = points[i];

CGFloat ascent;

CGFloat descent;

CGRect boundsRun;

boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);

boundsRun.size.height = ascent + descent;

CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);

boundsRun.origin.x = point.x + xOffset;

boundsRun.origin.y = point.y - descent;

CGPathRef path = CTFrameGetPath(frame);

CGRect colRect = CGPathGetBoundingBox(path);

CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);

return imageBounds;

}

}

return CGRectZero;

}

@end

原理分析:

1.你需要弄清楚CoreText的坐標(biāo)系跟系統(tǒng)坐標(biāo)系,他們兩者是反的,如果你不變換坐標(biāo)系的話(huà),你會(huì)發(fā)現(xiàn)你的問(wèn)題跟圖片都是反的

2.CoreText實(shí)現(xiàn)圖文混排其實(shí)就是在富文本中插入一個(gè)空白的圖片占位符的富文本字符串,通過(guò)代理設(shè)置相關(guān)的圖片尺寸信息,根據(jù)從富文本得到的frame計(jì)算圖片繪制的frame再繪制圖片這么一個(gè)過(guò)程。

代碼分析

1.翻轉(zhuǎn)坐標(biāo)系

CGContextRef context =UIGraphicsGetCurrentContext();//獲取當(dāng)前上下文

CGContextSetTextMatrix(context, CGAffineTransformIdentity);//設(shè)置字形的變換矩陣為不做圖形變換

CGContextTranslateCTM(context, 0, self.bounds.size.height);//平移方法,將畫(huà)布向上平移一個(gè)屏幕高

CGContextScaleCTM(context, 1.0, -1.0);//縮放方法,x軸縮放系數(shù)為1,則不變,y軸縮放系數(shù)為-1,則相當(dāng)于以x軸為軸旋轉(zhuǎn)180度

因?yàn)閏oreText使用的是系統(tǒng)坐標(biāo),然而我們平時(shí)所接觸的iOS的都是屏幕坐標(biāo),所以要將屏幕坐標(biāo)系轉(zhuǎn)換系統(tǒng)坐標(biāo)系,這樣才能與我們想想的坐標(biāo)互相對(duì)應(yīng)。

事實(shí)上呢,這三句是翻轉(zhuǎn)畫(huà)布的固定寫(xiě)法,這三句你以后會(huì)經(jīng)??吹降?。

2.設(shè)置富文本

NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\n這里在測(cè)試圖文混排,\n我是一個(gè)富文本"];//這句不用我多說(shuō)吧,最起碼得有個(gè)富文本啊才能插入不是。

3.圖片設(shè)置代理

事實(shí)上,圖文混排就是在要插入圖片的位置插入一個(gè)富文本類(lèi)型的占位符。通過(guò)CTRUNDelegate設(shè)置圖片

/*

設(shè)置一個(gè)回調(diào)結(jié)構(gòu)體,告訴代理該回調(diào)那些方法

*/

CTRunDelegateCallbacks callBacks;//創(chuàng)建一個(gè)回調(diào)結(jié)構(gòu)體,設(shè)置相關(guān)參數(shù)

memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));//memset將已開(kāi)辟內(nèi)存空間 callbacks 的首 n 個(gè)字節(jié)的值設(shè)為值 0, 相當(dāng)于對(duì)CTRunDelegateCallbacks內(nèi)存空間初始化

callBacks.version = kCTRunDelegateVersion1;//設(shè)置回調(diào)版本,默認(rèn)這個(gè)

callBacks.getAscent = ascentCallBacks;//設(shè)置圖片頂部距離基線(xiàn)的距離

callBacks.getDescent = descentCallBacks;//設(shè)置圖片底部距離基線(xiàn)的距離

callBacks.getWidth = widthCallBacks;//設(shè)置圖片寬度

4.設(shè)置圖片尺寸

上邊只是設(shè)置了圖片的代理還沒(méi)有設(shè)置圖片的尺寸,這里還需要一個(gè)知識(shí)點(diǎn)

正在上傳...取消

這呢就是一個(gè)CTRun的尺寸圖

一會(huì)我們繪制圖片的時(shí)候?qū)嶋H上實(shí)在一個(gè)CTRun中繪制這個(gè)圖片,那么CTRun繪制的坐標(biāo)系中,他會(huì)以origin點(diǎn)作為原點(diǎn)進(jìn)行繪制。

基線(xiàn)為過(guò)原點(diǎn)的x軸,ascent即為CTRun頂線(xiàn)距基線(xiàn)的距離,descent即為底線(xiàn)距基線(xiàn)的距離。

我們繪制圖片應(yīng)該從原點(diǎn)開(kāi)始繪制,圖片的高度及寬度及CTRun的高度及寬度,我們通過(guò)代理設(shè)置CTRun的尺寸間接設(shè)置圖片的尺寸。

/*

創(chuàng)建一個(gè)代理

*/

NSDictionary * dicPic = @{@"height":@50,@"width":@50};//創(chuàng)建一個(gè)圖片尺寸的字典,初始化代理對(duì)象需要

CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);//創(chuàng)建代理

上面只是設(shè)置了回調(diào)結(jié)構(gòu)體,然而我們還沒(méi)有告訴這個(gè)代理我們要的圖片尺寸。

所以這句話(huà)就在設(shè)置代理的時(shí)候綁定了一個(gè)返回圖片尺寸的字典。

事實(shí)上此處你可以綁定任意對(duì)象。此處你綁定的對(duì)象既是回調(diào)方法中的參數(shù)ref。

5.圖片的插入

首先創(chuàng)建一個(gè)富文本類(lèi)型的圖片占位符,綁定我們的代理

unichar placeHolder = 0xFFFC;//創(chuàng)建空白字符

NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];//已空白字符生成字符串

NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];//用字符串初始化占位符的富文本

CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);//給字符串中的范圍中字符串設(shè)置代理

CFRelease(delegate);//釋放(__bridge進(jìn)行C與OC數(shù)據(jù)類(lèi)型的轉(zhuǎn)換,C為非ARC,需要手動(dòng)管理)

然后將占位符插入到我們的富文本中

[attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];//將占位符插入原富文本

6.繪制

繪制分成兩部分,繪制文本和繪制圖片。

為什么要分兩部分呢?

因?yàn)楦晃谋局心闾砑拥膱D片只是一個(gè)帶有圖片尺寸的空白占位符啊,你繪制的時(shí)候他只會(huì)繪制出相應(yīng)尺寸的空白占位符,所以什么也顯示不了啊。

那怎么顯示圖片啊?拿到占位符的坐標(biāo),在占位符的地方繪制相應(yīng)大小的圖片就好了。

6.1繪制文本

CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);//一個(gè)frame的工廠(chǎng),負(fù)責(zé)生成frame

CGMutablePathRef path = CGPathCreateMutable();//創(chuàng)建繪制區(qū)域

CGPathAddRect(path, NULL, self.bounds);//添加繪制尺寸

NSInteger length = attributeStr.length;

CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,length), path, NULL);//工廠(chǎng)根據(jù)繪制區(qū)域及富文本(可選范圍,多次設(shè)置)設(shè)置frame

CTFrameDraw(frame, context);//根據(jù)frame繪制文字

frameSetter是根據(jù)富文本生成的一個(gè)frame生成的工廠(chǎng),你可以通過(guò)framesetter以及你想要繪制的富文本的范圍獲取該CTRun的frame。

但是你需要注意的是,獲取的frame是僅繪制你所需要的那部分富文本的frame。即當(dāng)前情況下,你繪制范圍定為(10,1),那么你得到的尺寸是只繪制(10,1)的尺寸,他應(yīng)該從屏幕左上角開(kāi)始(因?yàn)槟愀淖兞俗鴺?biāo)系),而不是當(dāng)你繪制全部富文本時(shí)他該在的位置。

然后建立一會(huì)繪制的尺寸,實(shí)際上就是在指定你的繪制范圍。

接著生成整個(gè)富文本繪制所需要的frame。因?yàn)榉秶侨课谋?,所以獲取的frame即為全部文本的frame(此處老司機(jī)希望你一定要搞清楚全部與指定范圍獲取的frame他們都是從左上角開(kāi)始的,否則你會(huì)進(jìn)入一個(gè)奇怪的誤區(qū),稍后會(huì)提到的)。

最后,根據(jù)你獲得的frame,繪制全部富文本。

6.2繪制圖片

上面你已經(jīng)繪制出文字,不過(guò)沒(méi)有圖片哦,接下來(lái)繪制圖片。

繪制圖片用下面這個(gè)方法,通用的哦

CGContextDrawImage(context,imgFrm, image.CGImage);//繪制圖片

我們可以看到這個(gè)方法有三個(gè)參數(shù),分別是context,frame,以及image。

要什么就給他什么好咯,context和image都好說(shuō),context就是當(dāng)前的上下文,最開(kāi)始獲得那個(gè)。image就是你要添加的那個(gè)圖片,不過(guò)是CGImage類(lèi)型。通過(guò)UIImage轉(zhuǎn)出CGImage就好了,我們重點(diǎn)講一下frame的獲取。

frame的獲取

記得我之前說(shuō)的誤區(qū)么?這里我們要獲得Image的frame,你有沒(méi)有想過(guò)我們的frameSetter?

我也想過(guò),不過(guò)就像我說(shuō)的,你單獨(dú)用frameSetter求出的image的frame是不正確的,那是只繪制image而得的坐標(biāo),所以哪種方法不能用哦,要用下面的方法。

你們一定發(fā)現(xiàn),我獲取frame的方法單獨(dú)寫(xiě)了一個(gè)方法,為什么呢?

1.將代碼分離,方便修改。

2.最主要的是這部分代碼到哪里都能用,達(dá)到復(fù)用效果。

NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);//根據(jù)frame獲取需要繪制的線(xiàn)的數(shù)組

NSInteger count = [arrLines count];//獲取線(xiàn)的數(shù)量

CGPoint points[count];//建立起點(diǎn)的數(shù)組(cgpoint類(lèi)型為結(jié)構(gòu)體,故用C語(yǔ)言的數(shù)組)

CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);//獲取起點(diǎn)

第一句呢,獲取繪制frame中的所有CTLine。CTLine,又不知道了吧,老司機(jī)又要無(wú)恥的盜圖了。

CTFrame組成

上面呢,我們能看到一個(gè)CTFrame繪制的原理。

CTLine 可以看做Core Text繪制中的一行的對(duì)象 通過(guò)它可以獲得當(dāng)前行的line ascent,line descent ,line leading,還可以獲得Line下的所有Glyph Runs

CTRun 或者叫做 Glyph Run,是一組共享想相同attributes(屬性)的字形的集合體

一個(gè)CTFrame有幾個(gè)CTLine組成,有幾行文字就有幾行CTLine。一個(gè)CTLine有包含多個(gè)CTRun,一個(gè)CTRun是所有屬性都相同的那部分富文本的繪制單元。所以CTRun是CTFrame的基本繪制單元。

接著說(shuō)我們的代碼。

為什么我獲取的數(shù)組需要進(jìn)行類(lèi)型轉(zhuǎn)換呢?因?yàn)镃TFrameGetLines()返回值是CFArrayRef類(lèi)型的數(shù)據(jù)。就是一個(gè)c的數(shù)組類(lèi)型吧,暫且先這么理解,所以需要轉(zhuǎn)換。

那為什么不用__bridge呢?記得么,我說(shuō)過(guò),本身就傳地址的數(shù)據(jù)是不用橋接的。就是這樣。

然后獲取數(shù)組的元素個(gè)數(shù)。有什么用呢,因?yàn)槲覀円玫矫總€(gè)CTLine的原點(diǎn)坐標(biāo)進(jìn)行計(jì)算。每個(gè)CTLine都有自己的origin。所以要生成一個(gè)相同元素個(gè)數(shù)的數(shù)組去盛放origin對(duì)象。

然后用CTFrameGetLineOrigins獲取所有原點(diǎn)。

到此,我們計(jì)算frame的準(zhǔn)備工作完成了。才完成準(zhǔn)備工作。

計(jì)算frame

思路呢,就是遍歷我們的frame中的所有CTRun,檢查他是不是我們綁定圖片的那個(gè),如果是,根據(jù)該CTRun所在CTLine的origin以及CTRun在CTLine中的橫向偏移量計(jì)算出CTRun的原點(diǎn),加上其尺寸即為該CTRun的尺寸。

跟繞口令是的,不過(guò)就是這么個(gè)思路。

for (int i = 0; i < count; i ++) {//遍歷線(xiàn)的數(shù)組

CTLineRef line = (__bridge CTLineRef)arrLines[i];

NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);//獲取GlyphRun數(shù)組(GlyphRun:高效的字符繪制方案)

for (int j = 0; j < arrGlyphRun.count; j ++) {//遍歷CTRun數(shù)組

CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];//獲取CTRun

NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);//獲取CTRun的屬性

CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];//獲取代理

if (delegate == nil) {//非空

continue;

}

NSDictionary * dic = CTRunDelegateGetRefCon(delegate);//判斷代理字典

if (![dic isKindOfClass:[NSDictionary class]]) {

continue;

}

CGPoint point = points[i];//獲取一個(gè)起點(diǎn)

CGFloat ascent;//獲取上距

CGFloat descent;//獲取下距

CGRect boundsRun;//創(chuàng)建一個(gè)frame

boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);

boundsRun.size.height = ascent + descent;//取得高

CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);//獲取x偏移量

boundsRun.origin.x = point.x + xOffset;//point是行起點(diǎn)位置,加上每個(gè)字的偏移量得到每個(gè)字的x

boundsRun.origin.y = point.y - descent;//計(jì)算原點(diǎn)

CGPathRef path = CTFrameGetPath(frame);//獲取繪制區(qū)域

CGRect colRect = CGPathGetBoundingBox(path);//獲取剪裁區(qū)域邊框

CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);

return imageBounds;

有了上面的思路這里就很好理解了。

外層for循環(huán)呢,是為了取到所有的CTLine。

類(lèi)型轉(zhuǎn)換什么的我就不多說(shuō)了,然后通過(guò)CTLineGetGlyphRuns獲取一個(gè)CTLine中的所有CTRun。

里層for循環(huán)是檢查每個(gè)CTRun。

通過(guò)CTRunGetAttributes拿到該CTRun的所有屬性。

通過(guò)kvc取得屬性中的代理屬性。

接下來(lái)判斷代理屬性是否為空。因?yàn)閳D片的占位符我們是綁定了代理的,而文字沒(méi)有。以此區(qū)分文字和圖片。

如果代理不為空,通過(guò)CTRunDelegateGetRefCon取得生成代理時(shí)綁定的對(duì)象。判斷類(lèi)型是否是我們綁定的類(lèi)型,防止取得我們之前為其他的富文本綁定過(guò)代理。

如果兩條都符合,ok,這就是我們要的那個(gè)CTRun。

開(kāi)始計(jì)算該CTRun的frame吧。

獲取原點(diǎn)和獲取寬高被。

通過(guò)CTRunGetTypographicBounds取得寬,ascent和descent。有了上面的介紹我們應(yīng)該知道圖片的高度就是ascent+descent了吧。

接下來(lái)獲取原點(diǎn)。

CTLineGetOffsetForStringIndex獲取對(duì)應(yīng)CTRun的X偏移量。

取得對(duì)應(yīng)CTLine的原點(diǎn)的Y,減去圖片的下邊距才是圖片的原點(diǎn),這點(diǎn)應(yīng)該很好理解。

至此,我們已經(jīng)獲得了圖片的frame了。因?yàn)橹唤壎艘粋€(gè)圖片,所以直接return就好了,如果多張圖片可以繼續(xù)遍歷返回?cái)?shù)組。

獲取到圖片的frame,我們就可以繪制圖片了,用上面介紹的方法。

在此我借鑒了老司機(jī)Wicky的簡(jiǎn)書(shū),對(duì)我?guī)椭艽螅乙餐ㄟ^(guò)他的博客做了自己的自我總結(jié)

參考文章:

http://www.lxweimin.com/p/6db3289fb05d

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一個(gè)老司機(jī)的 Coretext圖文混排 非常容易理解 簡(jiǎn)書(shū):老司機(jī)Wicky 可以點(diǎn)擊查看. //注:對(duì)上面那位老...
    我的大名叫小愛(ài)閱讀 603評(píng)論 0 0
  • 系列文章: CoreText實(shí)現(xiàn)圖文混排 CoreText實(shí)現(xiàn)圖文混排之點(diǎn)擊事件 CoreText實(shí)現(xiàn)圖文混排之文...
    老司機(jī)Wicky閱讀 40,309評(píng)論 221 432
  • 0. 基本知識(shí)準(zhǔn)備 0.1 字形( Glyph)基本了解 基礎(chǔ)原點(diǎn)(Origin)首先是位于基線(xiàn)上處于基線(xiàn)最左側(cè)的...
    破弓閱讀 3,198評(píng)論 4 24
  • 文字排版的基礎(chǔ)概念 字體(Font):和我們平時(shí)說(shuō)的字體不同,計(jì)算機(jī)意義上的字體表示的是同一大小,同一樣式(Sty...
    iOS白水閱讀 683評(píng)論 0 0
  • 隨著ios9的推出和Xcode的升級(jí),apple將默認(rèn)開(kāi)發(fā)者使用https的傳輸方式,相比http的傳輸協(xié)議...
    親親qin閱讀 556評(píng)論 0 1