8. CoreText

蘋果文檔 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é)部分。


image.png

一、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也分為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:移除屬性
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)換,則文字從下開始,還是倒著的
    如下圖(盜的圖,別打我)
 */

image.png
這四句什么意思呢?
首先第一句。
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è)距離什么東西呢?

image.png

對(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ī)又要無恥的盜圖了。

image.png

上面呢,我們能看到一個(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原理及基本使用方法

# iOS:基于CoreText的排版引擎

CoreText分頁

TextKit 實(shí)現(xiàn)文本分頁

下面也提供一下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地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,414評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,465評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,823評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,000評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,554評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,513評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,722評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,237評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,482評(píng)論 2 379

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