最近在網(wǎng)上看了一些大牛的文章,自己也試著寫了一下,感覺圖文混排真的很強大。
廢話不多說,開始整
先上效果圖跟代碼,然后一步步一句句給你分析
這個圖是我的APP上面做的一個示范,后面內(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這里在測試圖文混排,\n我是一個富文本"];
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:@"默認頭像"];
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的坐標系跟系統(tǒng)坐標系,他們兩者是反的,如果你不變換坐標系的話,你會發(fā)現(xiàn)你的問題跟圖片都是反的
2.CoreText實現(xiàn)圖文混排其實就是在富文本中插入一個空白的圖片占位符的富文本字符串,通過代理設(shè)置相關(guān)的圖片尺寸信息,根據(jù)從富文本得到的frame計算圖片繪制的frame再繪制圖片這么一個過程。
代碼分析
1.翻轉(zhuǎn)坐標系
CGContextRef context =UIGraphicsGetCurrentContext();//獲取當前上下文
CGContextSetTextMatrix(context, CGAffineTransformIdentity);//設(shè)置字形的變換矩陣為不做圖形變換
CGContextTranslateCTM(context, 0, self.bounds.size.height);//平移方法,將畫布向上平移一個屏幕高
CGContextScaleCTM(context, 1.0, -1.0);//縮放方法,x軸縮放系數(shù)為1,則不變,y軸縮放系數(shù)為-1,則相當于以x軸為軸旋轉(zhuǎn)180度
因為coreText使用的是系統(tǒng)坐標,然而我們平時所接觸的iOS的都是屏幕坐標,所以要將屏幕坐標系轉(zhuǎn)換系統(tǒng)坐標系,這樣才能與我們想想的坐標互相對應。
事實上呢,這三句是翻轉(zhuǎn)畫布的固定寫法,這三句你以后會經(jīng)常看到的。
2.設(shè)置富文本
NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\n這里在測試圖文混排,\n我是一個富文本"];//這句不用我多說吧,最起碼得有個富文本啊才能插入不是。
3.圖片設(shè)置代理
事實上,圖文混排就是在要插入圖片的位置插入一個富文本類型的占位符。通過CTRUNDelegate設(shè)置圖片
/*
設(shè)置一個回調(diào)結(jié)構(gòu)體,告訴代理該回調(diào)那些方法
*/
CTRunDelegateCallbacks callBacks;//創(chuàng)建一個回調(diào)結(jié)構(gòu)體,設(shè)置相關(guān)參數(shù)
memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));//memset將已開辟內(nèi)存空間 callbacks 的首 n 個字節(jié)的值設(shè)為值 0, 相當于對CTRunDelegateCallbacks內(nèi)存空間初始化
callBacks.version = kCTRunDelegateVersion1;//設(shè)置回調(diào)版本,默認這個
callBacks.getAscent = ascentCallBacks;//設(shè)置圖片頂部距離基線的距離
callBacks.getDescent = descentCallBacks;//設(shè)置圖片底部距離基線的距離
callBacks.getWidth = widthCallBacks;//設(shè)置圖片寬度
4.設(shè)置圖片尺寸
上邊只是設(shè)置了圖片的代理還沒有設(shè)置圖片的尺寸,這里還需要一個知識點
正在上傳...取消
這呢就是一個CTRun的尺寸圖
一會我們繪制圖片的時候?qū)嶋H上實在一個CTRun中繪制這個圖片,那么CTRun繪制的坐標系中,他會以origin點作為原點進行繪制。
基線為過原點的x軸,ascent即為CTRun頂線距基線的距離,descent即為底線距基線的距離。
我們繪制圖片應該從原點開始繪制,圖片的高度及寬度及CTRun的高度及寬度,我們通過代理設(shè)置CTRun的尺寸間接設(shè)置圖片的尺寸。
/*
創(chuàng)建一個代理
*/
NSDictionary * dicPic = @{@"height":@50,@"width":@50};//創(chuàng)建一個圖片尺寸的字典,初始化代理對象需要
CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);//創(chuàng)建代理
上面只是設(shè)置了回調(diào)結(jié)構(gòu)體,然而我們還沒有告訴這個代理我們要的圖片尺寸。
所以這句話就在設(shè)置代理的時候綁定了一個返回圖片尺寸的字典。
事實上此處你可以綁定任意對象。此處你綁定的對象既是回調(diào)方法中的參數(shù)ref。
5.圖片的插入
首先創(chuàng)建一個富文本類型的圖片占位符,綁定我們的代理
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進行C與OC數(shù)據(jù)類型的轉(zhuǎn)換,C為非ARC,需要手動管理)
然后將占位符插入到我們的富文本中
[attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];//將占位符插入原富文本
6.繪制
繪制分成兩部分,繪制文本和繪制圖片。
為什么要分兩部分呢?
因為富文本中你添加的圖片只是一個帶有圖片尺寸的空白占位符啊,你繪制的時候他只會繪制出相應尺寸的空白占位符,所以什么也顯示不了啊。
那怎么顯示圖片啊?拿到占位符的坐標,在占位符的地方繪制相應大小的圖片就好了。
6.1繪制文本
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);//一個frame的工廠,負責生成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);//工廠根據(jù)繪制區(qū)域及富文本(可選范圍,多次設(shè)置)設(shè)置frame
CTFrameDraw(frame, context);//根據(jù)frame繪制文字
frameSetter是根據(jù)富文本生成的一個frame生成的工廠,你可以通過framesetter以及你想要繪制的富文本的范圍獲取該CTRun的frame。
但是你需要注意的是,獲取的frame是僅繪制你所需要的那部分富文本的frame。即當前情況下,你繪制范圍定為(10,1),那么你得到的尺寸是只繪制(10,1)的尺寸,他應該從屏幕左上角開始(因為你改變了坐標系),而不是當你繪制全部富文本時他該在的位置。
然后建立一會繪制的尺寸,實際上就是在指定你的繪制范圍。
接著生成整個富文本繪制所需要的frame。因為范圍是全部文本,所以獲取的frame即為全部文本的frame(此處老司機希望你一定要搞清楚全部與指定范圍獲取的frame他們都是從左上角開始的,否則你會進入一個奇怪的誤區(qū),稍后會提到的)。
最后,根據(jù)你獲得的frame,繪制全部富文本。
6.2繪制圖片
上面你已經(jīng)繪制出文字,不過沒有圖片哦,接下來繪制圖片。
繪制圖片用下面這個方法,通用的哦
CGContextDrawImage(context,imgFrm, image.CGImage);//繪制圖片
我們可以看到這個方法有三個參數(shù),分別是context,frame,以及image。
要什么就給他什么好咯,context和image都好說,context就是當前的上下文,最開始獲得那個。image就是你要添加的那個圖片,不過是CGImage類型。通過UIImage轉(zhuǎn)出CGImage就好了,我們重點講一下frame的獲取。
frame的獲取
記得我之前說的誤區(qū)么?這里我們要獲得Image的frame,你有沒有想過我們的frameSetter?
我也想過,不過就像我說的,你單獨用frameSetter求出的image的frame是不正確的,那是只繪制image而得的坐標,所以哪種方法不能用哦,要用下面的方法。
你們一定發(fā)現(xiàn),我獲取frame的方法單獨寫了一個方法,為什么呢?
1.將代碼分離,方便修改。
2.最主要的是這部分代碼到哪里都能用,達到復用效果。
NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);//根據(jù)frame獲取需要繪制的線的數(shù)組
NSInteger count = [arrLines count];//獲取線的數(shù)量
CGPoint points[count];//建立起點的數(shù)組(cgpoint類型為結(jié)構(gòu)體,故用C語言的數(shù)組)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);//獲取起點
第一句呢,獲取繪制frame中的所有CTLine。CTLine,又不知道了吧,老司機又要無恥的盜圖了。
CTFrame組成
上面呢,我們能看到一個CTFrame繪制的原理。
CTLine 可以看做Core Text繪制中的一行的對象 通過它可以獲得當前行的line ascent,line descent ,line leading,還可以獲得Line下的所有Glyph Runs
CTRun 或者叫做 Glyph Run,是一組共享想相同attributes(屬性)的字形的集合體
一個CTFrame有幾個CTLine組成,有幾行文字就有幾行CTLine。一個CTLine有包含多個CTRun,一個CTRun是所有屬性都相同的那部分富文本的繪制單元。所以CTRun是CTFrame的基本繪制單元。
接著說我們的代碼。
為什么我獲取的數(shù)組需要進行類型轉(zhuǎn)換呢?因為CTFrameGetLines()返回值是CFArrayRef類型的數(shù)據(jù)。就是一個c的數(shù)組類型吧,暫且先這么理解,所以需要轉(zhuǎn)換。
那為什么不用__bridge呢?記得么,我說過,本身就傳地址的數(shù)據(jù)是不用橋接的。就是這樣。
然后獲取數(shù)組的元素個數(shù)。有什么用呢,因為我們要用到每個CTLine的原點坐標進行計算。每個CTLine都有自己的origin。所以要生成一個相同元素個數(shù)的數(shù)組去盛放origin對象。
然后用CTFrameGetLineOrigins獲取所有原點。
到此,我們計算frame的準備工作完成了。才完成準備工作。
計算frame
思路呢,就是遍歷我們的frame中的所有CTRun,檢查他是不是我們綁定圖片的那個,如果是,根據(jù)該CTRun所在CTLine的origin以及CTRun在CTLine中的橫向偏移量計算出CTRun的原點,加上其尺寸即為該CTRun的尺寸。
跟繞口令是的,不過就是這么個思路。
for (int i = 0; i < count; i ++) {//遍歷線的數(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];//獲取一個起點
CGFloat ascent;//獲取上距
CGFloat descent;//獲取下距
CGRect boundsRun;//創(chuàng)建一個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是行起點位置,加上每個字的偏移量得到每個字的x
boundsRun.origin.y = point.y - descent;//計算原點
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。
類型轉(zhuǎn)換什么的我就不多說了,然后通過CTLineGetGlyphRuns獲取一個CTLine中的所有CTRun。
里層for循環(huán)是檢查每個CTRun。
通過CTRunGetAttributes拿到該CTRun的所有屬性。
通過kvc取得屬性中的代理屬性。
接下來判斷代理屬性是否為空。因為圖片的占位符我們是綁定了代理的,而文字沒有。以此區(qū)分文字和圖片。
如果代理不為空,通過CTRunDelegateGetRefCon取得生成代理時綁定的對象。判斷類型是否是我們綁定的類型,防止取得我們之前為其他的富文本綁定過代理。
如果兩條都符合,ok,這就是我們要的那個CTRun。
開始計算該CTRun的frame吧。
獲取原點和獲取寬高被。
通過CTRunGetTypographicBounds取得寬,ascent和descent。有了上面的介紹我們應該知道圖片的高度就是ascent+descent了吧。
接下來獲取原點。
CTLineGetOffsetForStringIndex獲取對應CTRun的X偏移量。
取得對應CTLine的原點的Y,減去圖片的下邊距才是圖片的原點,這點應該很好理解。
至此,我們已經(jīng)獲得了圖片的frame了。因為只綁定了一個圖片,所以直接return就好了,如果多張圖片可以繼續(xù)遍歷返回數(shù)組。
獲取到圖片的frame,我們就可以繪制圖片了,用上面介紹的方法。
在此我借鑒了老司機Wicky的簡書,對我?guī)椭艽螅乙餐ㄟ^他的博客做了自己的自我總結(jié)
參考文章:
http://www.lxweimin.com/p/6db3289fb05d