蘋果文檔 https://developer.apple.com/documentation/coretext
Core Text是和Core Graphics配合使用的,一般是在UIView的drawRect方法中的Graphics Context上進(jìn)行繪制的。 且Core Text真正負(fù)責(zé)繪制的是文本部分,圖片還是需要自己去手動(dòng)繪制,所以你必須關(guān)注很多繪制的細(xì)節(jié)部分。
一、CoreText框架
CoreText 框架中最常用的幾個(gè)類:
(1)、CTFont
(2)、CTFontCollection
(3)、CTFontDescriptor
(4)、CTFrame
(5)、CTFramesetter
(6)、CTGlyphInfo
(7)、CTLine
(8)、CTParagraphStyle
(9)、CTRun
(10)、CTTextTab
(11)、CTTypesetter
CoreText的介紹
Core Text 是基于 iOS 3.2+ 和 OSX 10.5+ 的一種能夠?qū)ξ谋靖袷胶臀谋静季诌M(jìn)行精細(xì)控制的文本引擎。它良好的結(jié)合了 UIKit 和 Core Graphics/Quartz:
UIKit 的 UILabel 允許你通過在 IB 中簡單的拖曳添加文本,但你不能改變文本的顏色和其中的單詞。 Core Graphics/Quartz幾乎允許你做任何系統(tǒng)允許的事情,但你需要為每個(gè)字形計(jì)算位置,并畫在屏幕上。
Core Text 正結(jié)合了這兩者!你可以完全控制位置、布局、類似文本大小和顏色這樣的屬性,而 Core Text 將幫你完善其它的東西——類似文本換行、字體呈現(xiàn)等等。
CoreText基礎(chǔ)用法
//1.獲取當(dāng)前設(shè)備上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//2.翻轉(zhuǎn)坐標(biāo)系(coreText坐標(biāo)系是以左下角為坐標(biāo)原點(diǎn),UIKit以左上角為坐標(biāo)原點(diǎn))
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//3.設(shè)置繪制區(qū)域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(10, 20, self.bounds.size.width - 20, self.bounds.size.height - 40));
//4.設(shè)置文本內(nèi)容
NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"Hello world"];
//5.設(shè)置CTFrame
CTFramesetterRef ctFrameSetting = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFrameSetting, CFRangeMake(0, [attString length]), path, NULL);
//6.在CTFrame中繪制文本關(guān)聯(lián)到上下文
CTFrameDraw(ctFrame, context);
//7.釋放變量
CFRelease(path);
CFRelease(ctFrameSetting);
CFRelease(ctFrame);
TextKit
@interface XXMViewController (){
UIImageView *imageView;
UITextView *textView;
}
@end
@implementation XXMViewController
- (void)viewDidLoad
{
[super viewDidLoad];
XMView *view = [[XMView alloc] init];
view.frame = CGRectMake(10, 100, 300, 300);
view.backgroundColor = [UIColor redColor];
[self.view addSubview:view];
NSString *str = @"\tWhen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knowlages";//xxx為文字內(nèi)容
//初始化和定義位置
textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 20,self.view.frame.size.width-20, self.view.frame.size.height-30)];
//內(nèi)容賦值
textView.text = str;
//添加到屏幕上
[self.view addSubview:textView];
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(10, 80, 160, 100)];
imageView.backgroundColor = [UIColor orangeColor];
imageView.image = [UIImage imageNamed:@"bd_logo1"];
[self.view addSubview:imageView];
textView.textContainer.exclusionPaths = @[[self translatedBezierPath]];
}
- (UIBezierPath *)translatedBezierPath
{
CGRect imageRect = [textView convertRect:imageView.frame fromView:self.view];
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(imageRect.origin.x+5, imageRect.origin.y, imageRect.size.width-5, imageRect.size.height-5)];
return bezierPath;
}
@end
嚴(yán)肅的就是iOS7新推出的類庫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
。
富文本
富文本是什么呢?
簡單的說,附帶有每一個(gè)文字屬性的字符串
,就是富文本。
在iOS中,我們有一個(gè)專門的類來處理富文本 AttributeString
。
富文本的基本使用方法
AttributedString也分為NSAttributedString
和NSMutableAttributedString
兩個(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:
移除屬性
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是不行的。
是不是用起來很簡單,富文本,跟字典沒什么區(qū)別么。
CoreText繪制富文本
CoreText實(shí)現(xiàn)圖文混排其實(shí)就是在富文本中插入一個(gè)空白的圖片占位符
的富文本字符串,通過代理設(shè)相關(guān)的圖片尺寸信息
,根據(jù)從富文本得到的frame計(jì)算圖片繪制的frame再繪制圖片
這么一個(gè)過程。
先來整體代碼
#import "XMView.h"
#import <CoreText/CoreText.h>
@implementation XMView
-(void)drawRect:(CGRect)rect
{
[super drawRect:rect];
//1.獲取當(dāng)前上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//翻轉(zhuǎn)坐標(biāo)系步驟
//設(shè)置當(dāng)前文本矩陣
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//文本沿y軸移動(dòng)
CGContextTranslateCTM(context, 0, self.bounds.size.height);
//文本翻轉(zhuǎn)成為CoreText坐標(biāo)系
CGContextScaleCTM(context, 1, -1);
NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\tWhen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knowlages"];
//2.設(shè)置文本內(nèi)容的屬性
//1設(shè)置部分文字顏色
[attributeStr addAttribute:(id)kCTForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0 , 27)];
//2設(shè)置部分文字字體
CGFloat fontSize = 20;
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
[attributeStr addAttribute:(id)kCTFontAttributeName value:(__bridge id)fontRef range:NSMakeRange(0, 27)];
//3設(shè)置斜體
CTFontRef italicFontRef = CTFontCreateWithName((CFStringRef)[UIFont italicSystemFontOfSize:20].fontName, 16, NULL);
[attributeStr addAttribute:(id)kCTFontAttributeName value:(__bridge id)italicFontRef range:NSMakeRange(27, 9)];
//4設(shè)置下劃線
[attributeStr addAttribute:(id)kCTUnderlineStyleAttributeName value:(id)[NSNumber numberWithInteger:kCTUnderlineStyleDouble] range:NSMakeRange(36, 10)];
//5設(shè)置下劃線顏色
[attributeStr addAttribute:(id)kCTUnderlineColorAttributeName value:(id)[UIColor greenColor].CGColor range:NSMakeRange(36, 10)];
//6設(shè)置空心字
long number1 = 2;
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt8Type, &number1);
[attributeStr addAttribute:(id)kCTStrokeWidthAttributeName value:(__bridge id)numRef range:NSMakeRange(56, 10)];
//7設(shè)置字體間距
long number = 10;
CFNumberRef num = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt8Type, &number);
[attributeStr addAttribute:(id)kCTKernAttributeName value:(__bridge id)num range:NSMakeRange(40, 10)];
//8設(shè)置行間距
CGFloat lineSpacing = 10;
const CFIndex kNumberOfSettings = 3;
CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
{kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &lineSpacing}
};
CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
[attributeStr addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0, [attributeStr length])];
#pragma mark - 空白占位符及代理設(shè)置
//3.CTRunDelegateCallBacks:用于保存指針的結(jié)構(gòu)體,由CTRun delegate進(jìn)行回調(diào)
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};
//創(chuàng)建CTRunDelegate的代理
CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);
unichar placeHolder = 0xFFFC; //使用oxFFFC作為空白占位符
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);
//4.根據(jù)attributeStr生成CTFramesetterRef
//1.創(chuàng)建繪制區(qū)域,顯示的區(qū)域可以用CGMUtablePathRef生成任意的形狀
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
NSInteger length = attributeStr.length;
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);
CTFrameDraw(frame, context);
//5.繪制圖像
UIImage * image = [UIImage imageNamed:@"bd_logo1"];
CGRect imgFrm = [self calculateImageRectWithFrame:frame];
CGContextDrawImage(context,imgFrm, image.CGImage);
//6.釋放
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;
}
#pragma mark - CoreText基礎(chǔ)用法
/**
* 基礎(chǔ)的coreText用法
*/
- (void)baseCoreTextUsingMethod {
//1.獲取當(dāng)前設(shè)備上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//2.翻轉(zhuǎn)坐標(biāo)系(coreText坐標(biāo)系是以左下角為坐標(biāo)原點(diǎn),UIKit以左上角為坐標(biāo)原點(diǎn))
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//3.設(shè)置繪制區(qū)域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(10, 20, self.bounds.size.width - 20, self.bounds.size.height - 40));
//4.設(shè)置文本內(nèi)容
NSAttributedString *attributeStr = [[NSAttributedString alloc] initWithString:@"Hello world"];
//5.設(shè)置CTFrame
CTFramesetterRef ctFrameSetting = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFrameSetting, CFRangeMake(0, [attributeStr length]), path, NULL);
//6.在CTFrame中繪制文本關(guān)聯(lián)到上下文
CTFrameDraw(ctFrame, context);
//7.釋放變量
CFRelease(path);
CFRelease(ctFrameSetting);
CFRelease(ctFrame);
}
不瞞你說,我看著代碼都煩,也怕,所以放心,老司機(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)換,則文字從下開始,還是倒著的
如下圖(盜的圖,別打我)
*/
這四句什么意思呢?
首先第一句。
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這里在測試圖文混排,\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語法沖突我少打一個(gè)下劃線)
真沒有,事實(shí)上不是所有數(shù)據(jù)轉(zhuǎn)換的時(shí)候都需要__bridge。你要問我怎么區(qū)分?那好我告訴你,C中就是傳遞指針的數(shù)據(jù)就不用。比如說字符串,數(shù)組。原因老司機(jī)現(xiàn)在解釋不通,等我能組織好語言的。 - 為什么還要釋放?我是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語言的數(shù)組)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);//獲取起點(diǎn)
第一句呢,獲取繪制frame中的所有CTLine。CTLine,又不知道了吧,老司機(jī)又要無恥的盜圖了。
上面呢,我們能看到一個(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正常的初始化添加子視圖就可以了。
作者:老司機(jī)Wicky
鏈接:http://www.lxweimin.com/p/6db3289fb05d
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
參考:
CoreText(一):基本用法
系列文章:
- CoreText實(shí)現(xiàn)圖文混排
- CoreText實(shí)現(xiàn)圖文混排之點(diǎn)擊事件
- CoreText實(shí)現(xiàn)圖文混排之文字環(huán)繞及點(diǎn)擊算法
- CoreText實(shí)現(xiàn)圖文混排之尺寸估算及文本選擇
CoreText分頁
下面也提供一下CoreText的分頁方法,畢竟CoreText分頁的速度會(huì)快一點(diǎn),同樣在設(shè)置字體的時(shí)候,中文內(nèi)容要用<code>Heiti</code>或<code>PingFang</code>
//CoreText 分頁
+ (NSArray *)coreTextPaging:(NSAttributedString *)str textFrame:(CGRect)textFrame{
NSMutableArray *pagingResult = [NSMutableArray array];
CFAttributedStringRef cfAttStr = (__bridge CFAttributedStringRef)str;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(cfAttStr);
CGPathRef path = CGPathCreateWithRect(textFrame, NULL);
int textPos = 0;
NSUInteger strLength = [str length];
while (textPos < strLength) {
//設(shè)置路徑
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, NULL);
//生成frame
CFRange frameRange = CTFrameGetVisibleStringRange(frame);
NSRange ra = NSMakeRange(frameRange.location, frameRange.length);
//獲取范圍并轉(zhuǎn)換為NSRange,然后以NSAttributedString形式保存
[pagingResult addObject:[str attributedSubstringFromRange:ra]];
//移動(dòng)當(dāng)前文本位置
textPos += frameRange.length;
CFRelease(frame);
}
CGPathRelease(path);
CFRelease(framesetter);
return pagingResult;
}
簡單的demo:demo地址