最近在網(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