CoreText實(shí)現(xiàn)圖文混排

CoreText實(shí)現(xiàn)圖文混排

系列文章:


也好久沒來寫博客了,主要是最近也工作了,手頭的事有點(diǎn)多,一時(shí)間也就斷了,閑下來了我就來補(bǔ)博客了碌更,剛好最近也做了很多東西,放在這里也算給自己做個(gè)筆記吧。


2016.12.25補(bǔ)充:最新demo在第三篇文章末尾宪赶。


CoreText

最近公司做了一個(gè)項(xiàng)目,需要用到圖文混排技術(shù)脯燃。于是呢就瘋狂地在網(wǎng)上搜刮資料搂妻。
不過很不幸的是,百度的CoreText資料還是比較少滴辕棚,翻來覆去就那幾個(gè)版本欲主。
然而我又上不去谷歌,so逝嚎,困難重重啊扁瓢。
不過雖然資料少,不夠前輩們給的貢獻(xiàn)終于還是在我的努力下都被我消化了懈糯,然后我也來做個(gè)筆記涤妒。


CoreText的介紹

Core Text 是基于 iOS 3.2+ 和 OSX 10.5+ 的一種能夠?qū)ξ谋靖袷胶臀谋静季诌M(jìn)行精細(xì)控制的文本引擎。
它良好的結(jié)合了 UIKit 和 Core Graphics/Quartz:

UIKit 的 UILabel 允許你通過在 IB 中簡(jiǎn)單的拖曳添加文本赚哗,但你不能改變文本的顏色和其中的單詞她紫。  
Core Graphics/Quartz幾乎允許你做任何系統(tǒng)允許的事情,但你需要為每個(gè)字形計(jì)算位置屿储,并畫在屏幕上贿讹。
Core Text 正結(jié)合了這兩者!你可以完全控制位置够掠、布局民褂、類似文本大小和顏色這樣的屬性,而 Core Text 將幫你完善其它的東西——類似文本換行、字體呈現(xiàn)等等赊堪。

以上就是對(duì)CoreText的介紹面殖。


老司機(jī)對(duì)CoreText實(shí)現(xiàn)圖文混排的一些理解

老司機(jī)認(rèn)為,圖文混排中使用到的CoreText只是CoreText龐大體系中一個(gè)對(duì)富文本的增強(qiáng)的一部分哭廉。
我個(gè)人想法啊脊僚,我讀書少,理解的可能不到位遵绰,不過你咬我啊辽幌。

恩,我又逗逼了一波椿访,說好的大師氣質(zhì)呢乌企,下面開始嚴(yán)肅了啊。

嚴(yán)肅的就是iOS7新推出的類庫(kù)Textkit成玫,其實(shí)是在之前推出的CoreText上的封裝加酵,根據(jù)蘋果的說法,他們開發(fā)了兩年多才完成梁剔,而且他們?cè)陂_發(fā)時(shí)候也將表情混排作為一個(gè)使用案例進(jìn)行研究虽画,所以要實(shí)現(xiàn)表情混排將會(huì)非常容易

蘋果引入TextKit的目的并非要取代已有的CoreText框架荣病,雖然CoreText的主要作用也是用于文字的排版和渲染码撰,但它是一種先進(jìn)而又處于底層技術(shù),如果我們需要將文本內(nèi)容直接渲染到圖形上下文(Graphics context)時(shí)个盆,從性能和易用性來考慮脖岛,最佳方案就是使用CoreText
原理的東西學(xué)一學(xué)總沒有壞處颊亮。因此柴梆,還是有必要去學(xué)一學(xué)CoreText的。
那我們開始學(xué)習(xí)吧终惑。


富文本

老司機(jī)說過绍在,我要講的只是用來增強(qiáng)富文本的那一部分,那么富文本怎么使用呢雹有。

富文本是什么呢偿渡?

富文本格式(RTF)規(guī)范是為了便于在應(yīng)用程序之間輕松轉(zhuǎn)儲(chǔ)格式化文本和圖形的一種編碼方法。
現(xiàn)在霸奕,用戶可以利用特定轉(zhuǎn)換軟件溜宽,在不同系統(tǒng)如MS-DOS、Windows质帅、OS/2适揉、Macintosh和Power Macintosh的應(yīng)用程序之間轉(zhuǎn)移字處理文檔留攒。
RTF規(guī)范提供一種在不同的輸出設(shè)備、操作環(huán)境和操作系統(tǒng)之間交換文本和圖形的一種格式嫉嘀。
RTF使用ANSI, PC-8, Macintosh, 或IBM PC字符集控制文檔的表示法和格式化炼邀,包括屏幕顯示和打印
。憑借RTF規(guī)范吃沪,不同的操作系統(tǒng)和不同的軟件程序創(chuàng)建的文檔能夠在這些操作系統(tǒng)和應(yīng)用程序之間傳遞汤善。
將一個(gè)格式化的文件轉(zhuǎn)換為RTF文件的軟件稱為RTF書寫器。
RTF書寫器用于分離現(xiàn)有文本中的程序控制信息票彪,并且生成一個(gè)包含文本和與之相關(guān)的RTF組的新文件。
將RTF文件轉(zhuǎn)換成格式化文件的軟件則稱為RTF閱讀器不狮。

簡(jiǎn)單的說降铸,附帶有每一個(gè)文字屬性的字符串,就是富文本摇零。
在iOS中推掸,我們有一個(gè)專門的類來處理富文本 AttributeString


富文本的基本使用方法

誒驻仅,標(biāo)題越來越小了谅畅,都4個(gè)#號(hào)了,說明我扯遠(yuǎn)了啊噪服。不過要想使用CoreText不會(huì)富文本還是不行啊毡泻。

來吧。
AttributedString也分為NSAttributedStringNSMutableAttributedString兩個(gè)類粘优,類似于String仇味,我就不贅述了。
富文本本質(zhì)上沒有什么難度雹顺,只要給指定的字符串附上指定的屬性就好了丹墨。下面給出富文本的一些基本方法。

  • -initWithString:以NSString初始化一個(gè)富文本對(duì)象
  • -setAttributes:range:為富文本中的一段范圍添加一些屬性嬉愧,第一個(gè)參數(shù)是個(gè)NSDictionary字典贩挣,第二個(gè)參數(shù)是NSRange。
  • -addAttribute:value:range:添加一個(gè)屬性
  • -addAttributes:range:添加多個(gè)屬性
  • -removeAttribute:range:移除屬性
    額,老司機(jī)知道這么說不直觀没酣,來來來王财,上代碼。
NSDictionary * dic = @{NSFontAttributeName:[UIFont fontWithName:@"Zapfino" size:20],NSForegroundColorAttributeName:[UIColor redColor],NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle)};
    NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"0我是一個(gè)富文本四康,9聽說我有很多屬性搪搏,19I will try。32這里清除屬性."];
//    設(shè)置屬性
    [attributeStr setAttributes:dic range:NSMakeRange(0, attributeStr.length)];
//    添加屬性
    [attributeStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:30] range:NSMakeRange(9, 10)];
    [attributeStr addAttribute:NSForegroundColorAttributeName value:[UIColor cyanColor] range:NSMakeRange(13, 13)];
//    添加多個(gè)屬性
    NSDictionary * dicAdd = @{NSBackgroundColorAttributeName:[UIColor yellowColor],NSLigatureAttributeName:@1};
    [attributeStr addAttributes:dicAdd range:NSMakeRange(19, 13)];
//    移除屬性
    [attributeStr removeAttribute:NSFontAttributeName range:NSMakeRange(32, 9)];
    UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 400)];
    label.numberOfLines = 0;
    label.attributedText = attributeStr;

這里你要注意一下闪金,給label的一定是給他的attributedText屬性疯溺,你給text是不行的论颅。
是不是用起來很簡(jiǎn)單,富文本囱嫩,跟字典沒什么區(qū)別么恃疯。


CoreText繪制富文本

是不是終于進(jìn)入正題了。其實(shí)之所以說那么多墨闲,還是為了你看完就能保證會(huì)用啊今妄,否則你不會(huì)富文本你自己還要查找富文本相關(guān)資料。

Come On鸳碧!

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

先來整體代碼

-(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":@129,@"width":@400};
    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:@"bd_logo1"];
    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;
}

不瞞你說,我看著代碼都煩套利,也怕推励,所以放心,老司機(jī)會(huì)一句一句給你解釋的肉迫。


分段解析

準(zhǔn)備工作

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);    
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

先要來一個(gè)背景介紹哈

/*
 coreText 起初是為OSX設(shè)計(jì)的验辞,而OSX得坐標(biāo)原點(diǎn)是左下角,y軸正方向朝上喊衫。iOS中坐標(biāo)原點(diǎn)是左上角跌造,y軸正方向向下。
 若不進(jìn)行坐標(biāo)轉(zhuǎn)換格侯,則文字從下開始鼻听,還是倒著的
    如下圖(盜的圖,別打我)
 */
系統(tǒng)坐標(biāo)系
這四句什么意思呢联四?
首先第一句撑碴。
CGContextRef context = UIGraphicsGetCurrentContext();//獲取當(dāng)前繪制上下文

為什么要回去上下文呢?因?yàn)槲覀兯械睦L制操作都是在上下文上進(jìn)行繪制的朝墩。

然后剩下的三句醉拓。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);//設(shè)置字形的變換矩陣為不做圖形變換  
    CGContextTranslateCTM(context, 0, self.bounds.size.height);//平移方法,將畫布向上平移一個(gè)屏幕高  
    CGContextScaleCTM(context, 1.0, -1.0);//縮放方法收苏,x軸縮放系數(shù)為1亿卤,則不變,y軸縮放系數(shù)為-1鹿霸,則相當(dāng)于以x軸為軸旋轉(zhuǎn)180度

正如之上的背景說的排吴,coreText使用的是系統(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ì)經(jīng)吵都看到的。

繼續(xù)珊肃。


圖片的代理的設(shè)置

/*
  事實(shí)上荣刑,圖文混排就是在要插入圖片的位置插入一個(gè)富文本類型的占位符。通過CTRUNDelegate設(shè)置圖片
*/
NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\n這里在測(cè)試圖文混排伦乔,\n我是一個(gè)富文本"];//這句不用我多說吧厉亏,最起碼得有個(gè)富文本啊才能插入不是。
/*
 設(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將已開辟內(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è)置圖片頂部距離基線的距離
callBacks.getDescent = descentCallBacks;//設(shè)置圖片底部距離基線的距離
callBacks.getWidth = widthCallBacks;//設(shè)置圖片寬度

注意了斥杜,這里經(jīng)GreyLove提醒有重要改動(dòng),就是這里沥匈,添加了memset蔗喂,碼字的時(shí)候少忘碼了一句話。怪我粗心高帖,十分抱歉缰儿,我會(huì)通知每一個(gè)留言的同學(xué)。

為什么要設(shè)置一個(gè)回調(diào)結(jié)構(gòu)體呢散址?
因?yàn)閏oreText中大量的調(diào)用c的方法乖阵。事實(shí)上你會(huì)發(fā)現(xiàn)大部分跟系統(tǒng)底層有關(guān)的都需要調(diào)c的方法。所以設(shè)置代理要按照人家的方法來啊预麸。

看看這幾句代碼也很好懂瞪浸,就是注釋中寫的意思。
后三句分別就是說當(dāng)我需要走這些代理的時(shí)候都會(huì)走那些代理方法吏祸。
好吧对蒲,扯到這又要補(bǔ)充知識(shí)了。這個(gè)距離什么東西呢贡翘?

字形

對(duì)對(duì)蹈矮,這呢就是一個(gè)CTRun的尺寸圖,什么你問CTRun是啥鸣驱?還沒到那呢泛鸟,后面會(huì)詳細(xì)介紹。
在這你只要知道踊东,一會(huì)我們繪制圖片的時(shí)候?qū)嶋H上實(shí)在一個(gè)CTRun中繪制這個(gè)圖片北滥,那么CTRun繪制的坐標(biāo)系中刚操,他會(huì)以origin點(diǎn)作為原點(diǎn)進(jìn)行繪制。
基線為過原點(diǎn)的x軸碑韵,ascent即為CTRun頂線距基線的距離赡茸,descent即為底線距基線的距離
我們繪制圖片應(yīng)該從原點(diǎn)開始繪制祝闻,圖片的高度及寬度及CTRun的高度及寬度占卧,我們通過代理設(shè)置CTRun的尺寸間接設(shè)置圖片的尺寸。


/*
 創(chuàng)建一個(gè)代理
*/
    NSDictionary * dicPic = @{@"height":@129,@"width":@400};//創(chuàng)建一個(gè)圖片尺寸的字典联喘,初始化代理對(duì)象需要
    CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);//創(chuàng)建代理

上面只是設(shè)置了回調(diào)結(jié)構(gòu)體华蜒,然而我們還沒有告訴這個(gè)代理我們要的圖片尺寸
所以這句話就在設(shè)置代理的時(shí)候綁定了一個(gè)返回圖片尺寸的字典豁遭。
事實(shí)上此處你可以綁定任意對(duì)象叭喜。此處你綁定的對(duì)象既是回調(diào)方法中的參數(shù)ref

好吧就然說到這我就直接把那三個(gè)回調(diào)方法說了吧蓖谢,放在一起比較好理解一些捂蕴。

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];
}

上文說過,ref既是創(chuàng)建代理是綁定的對(duì)象闪幽。所以我們?cè)谶@里啥辨,從字典中分別取出圖片的寬和高

值得注意的是盯腌,由于是c的方法溉知,所以也沒有什么對(duì)象的概念。是一個(gè)指針類型的數(shù)據(jù)腕够。不過oc的對(duì)象其實(shí)也就是c的結(jié)構(gòu)體级乍。我們可以通過類型轉(zhuǎn)換獲得oc中的字典。
__bridge既是C的結(jié)構(gòu)體轉(zhuǎn)換成OC對(duì)象時(shí)需要的一個(gè)修飾詞帚湘。

老司機(jī)敲字慢啊玫荣,敲到這都兩個(gè)小時(shí)了,容我喝口水客们。

你們喝過紅色的尖叫么崇决?老司機(jī)喝了那種煙頭泡的水之后精神滿滿的繼續(xù)敲字。(那水超難喝底挫,你可以挑戰(zhàn)一下)
誒恒傻,說好的嚴(yán)肅呢?


圖片的插入

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

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ù)類型的轉(zhuǎn)換盈厘,C為非ARC,需要手動(dòng)管理)

這里富文本的知識(shí)上文中已經(jīng)介紹過了官边。不過老司機(jī)猜你有三個(gè)疑問沸手。

  • 這個(gè)添加屬性的方法怎么是這個(gè)樣子的外遇?
    因?yàn)檫@里是添加CTRunDelegate這種數(shù)據(jù)類型,要用CoreText專門的方法契吉,不過其實(shí)就是形式不同跳仿,作用一樣的。
  • 為什么這里富文本類型轉(zhuǎn)換的時(shí)候不用_bridge呢捐晶?老司機(jī)你不是說需要修飾詞么菲语?你是不是騙我?(markDown語(yǔ)法沖突我少打一個(gè)下劃線)
    真沒有惑灵,事實(shí)上不是所有數(shù)據(jù)轉(zhuǎn)換的時(shí)候都需要__bridge山上。你要問我怎么區(qū)分?那好我告訴你英支,C中就是傳遞指針的數(shù)據(jù)就不用佩憾。比如說字符串,數(shù)組干花。原因老司機(jī)現(xiàn)在解釋不通妄帘,等我能組織好語(yǔ)言的。
  • 為什么還要釋放池凄?我是ARC環(huán)境啊
    不好意思寄摆,我也是。不過為什么要釋放呢修赞?因?yàn)槟氵M(jìn)行了類型轉(zhuǎn)換之后就不屬于對(duì)象了,也不再歸自動(dòng)引用計(jì)數(shù)機(jī)制管理了桑阶,所以你得手動(dòng)管理咯柏副。

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

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

此處我就不贅述了,富文本的知識(shí)你只要類比字典就好了蚣录。
至此割择,我們已經(jīng)生成好了我們要的帶有圖片信息的富文本了,接下來我們只要在畫布上繪制出來這個(gè)富文本就好了萎河。


繪制

繪制呢荔泳,又分成兩部分,繪制文本繪制圖片虐杯。你問我為什么還分成兩個(gè)玛歌?

因?yàn)楦晃谋局心闾砑拥膱D片只是一個(gè)帶有圖片尺寸的空白占位符啊,你繪制的時(shí)候他只會(huì)繪制出相應(yīng)尺寸的空白占位符擎椰,所以什么也顯示不了啊支子。
那怎么顯示圖片啊达舒?拿到占位符的坐標(biāo)值朋,在占位符的地方繪制相應(yīng)大小的圖片就好了叹侄。恩,說到這昨登,圖文混排的原理已經(jīng)說完了趾代。

先來繪制文本吧。


繪制文本
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);//一個(gè)frame的工廠丰辣,負(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);//工廠根據(jù)繪制區(qū)域及富文本(可選范圍撒强,多次設(shè)置)設(shè)置frame
CTFrameDraw(frame, context);//根據(jù)frame繪制文字

frameSetter是根據(jù)富文本生成的一個(gè)frame生成的工廠,你可以通過framesetter以及你想要繪制的富文本的范圍獲取該CTRun的frame糯俗。
但是你需要注意的是尿褪,獲取的frame是僅繪制你所需要的那部分富文本的frame。即當(dāng)前情況下得湘,你繪制范圍定為(10杖玲,1),那么你得到的尺寸是只繪制(10淘正,1)的尺寸摆马,他應(yīng)該從屏幕左上角開始(因?yàn)槟愀淖兞俗鴺?biāo)系),而不是當(dāng)你繪制全部富文本時(shí)他該在的位置鸿吆。

然后建立一會(huì)繪制的尺寸囤采,實(shí)際上就是在指定你的繪制范圍
接著生成整個(gè)富文本繪制所需要的frame惩淳。因?yàn)榉秶侨课谋窘短海垣@取的frame即為全部文本的frame(此處老司機(jī)希望你一定要搞清楚全部與指定范圍獲取的frame他們都是從左上角開始的,否則你會(huì)進(jìn)入一個(gè)奇怪的誤區(qū)思犁,稍后會(huì)提到的)代虾。
最后,根據(jù)你獲得的frame激蹲,繪制全部富文本棉磨。


繪制圖片

上面你已經(jīng)繪制出文字,不過沒有圖片哦学辱,接下來繪制圖片乘瓤。
繪制圖片用下面這個(gè)方法,通用的哦

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

我們可以看到這個(gè)方法有三個(gè)參數(shù)策泣,分別是context衙傀,frame,以及image萨咕。
要什么就給他什么好咯差油,context和image都好說,context就是當(dāng)前的上下文,最開始獲得那個(gè)蓄喇。image就是你要添加的那個(gè)圖片发侵,不過是CGImage類型。通過UIImage轉(zhuǎn)出CGImage就好了妆偏,我們重點(diǎn)講一下frame的獲取刃鳄。

frame的獲取

記得我之前說的誤區(qū)么?這里我們要獲得Image的frame钱骂,你有沒有想過我們的frameSetter叔锐?

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

你們一定發(fā)現(xiàn)鹅搪,我獲取frame的方法單獨(dú)寫了一個(gè)方法禀苦,為什么呢蔓肯?
1.將代碼分離,方便修改振乏。
2.最主要的是這部分代碼到哪里都能用蔗包,達(dá)到復(fù)用效果。

NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);//根據(jù)frame獲取需要繪制的線的數(shù)組
NSInteger count = [arrLines count];//獲取線的數(shù)量
CGPoint points[count];//建立起點(diǎn)的數(shù)組(cgpoint類型為結(jié)構(gòu)體慧邮,故用C語(yǔ)言的數(shù)組)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);//獲取起點(diǎn)

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

CTFrame組成

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

  • CTLine 可以看做Core Text繪制中的一行的對(duì)象 通過它可以獲得當(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的基本繪制單元
接著說我們的代碼妙色。
為什么我獲取的數(shù)組需要進(jìn)行類型轉(zhuǎn)換呢桅滋?因?yàn)镃TFrameGetLines()返回值是CFArrayRef類型的數(shù)據(jù)。就是一個(gè)c的數(shù)組類型吧,暫且先這么理解丐谋,所以需要轉(zhuǎn)換芍碧。

那為什么不用__bridge呢?記得么号俐,我說過泌豆,本身就傳地址的數(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的尺寸颖榜。

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

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];//獲取一個(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且蓬。
類型轉(zhuǎn)換什么的我就不多說了欣硼,然后通過CTLineGetGlyphRuns獲取一個(gè)CTLine中的所有CTRun
里層for循環(huán)是檢查每個(gè)CTRun恶阴。
通過CTRunGetAttributes拿到該CTRun的所有屬性诈胜。
通過kvc取得屬性中的代理屬性
接下來判斷代理屬性是否為空冯事。因?yàn)閳D片的占位符我們是綁定了代理的焦匈,而文字沒有。以此區(qū)分文字和圖片昵仅。
如果代理不為空缓熟,通過CTRunDelegateGetRefCon取得生成代理時(shí)綁定的對(duì)象判斷類型是否是我們綁定的類型,防止取得我們之前為其他的富文本綁定過代理够滑。
如果兩條都符合垦写,ok,這就是我們要的那個(gè)CTRun版述。
開始計(jì)算該CTRun的frame吧梯澜。
獲取原點(diǎn)和獲取寬高被。
通過CTRunGetTypographicBounds取得寬渴析,ascent和descent晚伙。有了上面的介紹我們應(yīng)該知道圖片的高度就是ascent+descent了吧。
接下來獲取原點(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登颓,我們就可以繪制圖片了,用上面介紹的方法红氯。


哦框咙,別忘了手動(dòng)釋放你創(chuàng)建的對(duì)象哦。

CFRelease(frame);
CFRelease(path);
CFRelease(frameSetter);

大功告成痢甘。


好了喇嘱,至此你已經(jīng)完成圖片的繪制了。只要在ViewController里面引入你繪制CoreText文本的View正常的初始化添加子視圖就可以了塞栅。

好吧者铜,這個(gè)教程我也是綜合了很多資料寫出來的。優(yōu)勢(shì)是在于我一句一句講的放椰,幾乎每一句都告訴你原理了吧作烟。

恩,我也是在前人的基礎(chǔ)上自己總結(jié)查閱出來的庄敛,難免夾雜著個(gè)人理解和部分偏頗,如果各位看官發(fā)現(xiàn)我寫的有什么不對(duì)的地方歡迎與我聯(lián)系科汗,老司機(jī)的郵箱codewicky@163.com藻烤。

原諒老司機(jī)逗逼的本質(zhì),嚴(yán)肅不起來。

下面是一些參考資料:
coreText方法怖亭,列的很全
CTRun的詳細(xì)介紹
CTLine的詳細(xì)介紹
coreText基本原理及使用方法
圖文混排的整體介紹
coreText使用方法

你要是喜歡呢涎显,麻煩你動(dòng)一動(dòng)你可愛的小手點(diǎn)擊一下喜歡或者關(guān)注,畢竟老司機(jī)這么愛慕虛榮的人兴猩,而且老司機(jī)會(huì)經(jīng)常更新的期吓。

最后,你問我為什么一直叫自己老司機(jī)倾芝?哦讨勤,因?yàn)楹俸俸賬~~

哦,最后的最后晨另,若果真有人轉(zhuǎn)載的話潭千,麻煩你注明出處。
http://www.reibang.com/p/6db3289fb05d

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末借尿,一起剝皮案震驚了整個(gè)濱河市刨晴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌路翻,老刑警劉巖狈癞,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異茂契,居然都是意外死亡蝶桶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門账嚎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來莫瞬,“玉大人,你說我怎么就攤上這事郭蕉√垩” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵召锈,是天一觀的道長(zhǎng)旁振。 經(jīng)常有香客問我,道長(zhǎng)涨岁,這世上最難降的妖魔是什么拐袜? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮梢薪,結(jié)果婚禮上蹬铺,老公的妹妹穿的比我還像新娘。我一直安慰自己秉撇,他們只是感情好甜攀,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布秋泄。 她就那樣靜靜地躺著,像睡著了一般规阀。 火紅的嫁衣襯著肌膚如雪恒序。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天谁撼,我揣著相機(jī)與錄音歧胁,去河邊找鬼。 笑死厉碟,一個(gè)胖子當(dāng)著我的面吹牛喊巍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播墨榄,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼玄糟,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了袄秩?” 一聲冷哼從身側(cè)響起阵翎,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎之剧,沒想到半個(gè)月后郭卫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡背稼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年贰军,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蟹肘。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡词疼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出帘腹,到底是詐尸還是另有隱情贰盗,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布阳欲,位于F島的核電站舵盈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏球化。R本人自食惡果不足惜秽晚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望筒愚。 院中可真熱鬧赴蝇,春花似錦、人聲如沸巢掺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至熄阻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倔约,已是汗流浹背秃殉。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浸剩,地道東北人钾军。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像绢要,于是被迫代替她去往敵國(guó)和親吏恭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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